diff --git a/build/config.yml b/build/config.yml
index a3675d8..d4862ea 100644
--- a/build/config.yml
+++ b/build/config.yml
@@ -11,7 +11,7 @@ info:
description: "A fast, native SQL client built with Go, Wails and React." # The application description
copyright: "(c) 2026, Bare7a" # Copyright text
comments: "A fast, native SQL client built with Go, Wails and React." # Comments
- version: "1.4.2" # The application version
+ version: "1.4.3" # The application version
# cfBundleIconName: "appicon" # The macOS icon name in Assets.car icon bundles (optional)
# # Should match the name of your .icon file without the extension
# # If not set and Assets.car exists, defaults to "appicon"
diff --git a/build/darwin/Info.dev.plist b/build/darwin/Info.dev.plist
index df5da25..23fd15f 100644
--- a/build/darwin/Info.dev.plist
+++ b/build/darwin/Info.dev.plist
@@ -78,9 +78,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.2
+ 1.4.3
CFBundleVersion
- 1.4.2
+ 1.4.3
LSMinimumSystemVersion
12.0.0
NSAppTransportSecurity
diff --git a/build/darwin/Info.plist b/build/darwin/Info.plist
index 0656e0a..8cd5b12 100644
--- a/build/darwin/Info.plist
+++ b/build/darwin/Info.plist
@@ -78,9 +78,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.2
+ 1.4.3
CFBundleVersion
- 1.4.2
+ 1.4.3
LSMinimumSystemVersion
12.0.0
NSHighResolutionCapable
diff --git a/build/ios/Info.dev.plist b/build/ios/Info.dev.plist
index 72fce0a..60c5af1 100644
--- a/build/ios/Info.dev.plist
+++ b/build/ios/Info.dev.plist
@@ -15,9 +15,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.2-dev
+ 1.4.3-dev
CFBundleVersion
- 1.4.2
+ 1.4.3
LSRequiresIPhoneOS
MinimumOSVersion
diff --git a/build/ios/Info.plist b/build/ios/Info.plist
index a0e09f5..3f825ef 100644
--- a/build/ios/Info.plist
+++ b/build/ios/Info.plist
@@ -15,9 +15,9 @@
CFBundlePackageType
APPL
CFBundleShortVersionString
- 1.4.2
+ 1.4.3
CFBundleVersion
- 1.4.2
+ 1.4.3
LSRequiresIPhoneOS
MinimumOSVersion
diff --git a/build/linux/nfpm/nfpm.yaml b/build/linux/nfpm/nfpm.yaml
index f4e54ac..71b8bec 100644
--- a/build/linux/nfpm/nfpm.yaml
+++ b/build/linux/nfpm/nfpm.yaml
@@ -6,7 +6,7 @@
name: "XenSQL"
arch: ${GOARCH}
platform: "linux"
-version: "1.4.2"
+version: "1.4.3"
section: "default"
priority: "extra"
maintainer: ${GIT_COMMITTER_NAME} <${GIT_COMMITTER_EMAIL}>
diff --git a/build/windows/info.json b/build/windows/info.json
index 29e5922..5f5dc31 100644
--- a/build/windows/info.json
+++ b/build/windows/info.json
@@ -1,10 +1,10 @@
{
"fixed": {
- "file_version": "1.4.2"
+ "file_version": "1.4.3"
},
"info": {
"0000": {
- "ProductVersion": "1.4.2",
+ "ProductVersion": "1.4.3",
"CompanyName": "Bare7a",
"FileDescription": "A fast, native SQL client built with Go, Wails and React.",
"LegalCopyright": "(c) 2026, Bare7a",
diff --git a/build/windows/nsis/wails_tools.nsh b/build/windows/nsis/wails_tools.nsh
index 305b877..97548e0 100644
--- a/build/windows/nsis/wails_tools.nsh
+++ b/build/windows/nsis/wails_tools.nsh
@@ -14,7 +14,7 @@
!define INFO_PRODUCTNAME "XenSQL"
!endif
!ifndef INFO_PRODUCTVERSION
- !define INFO_PRODUCTVERSION "1.4.2"
+ !define INFO_PRODUCTVERSION "1.4.3"
!endif
!ifndef INFO_COPYRIGHT
!define INFO_COPYRIGHT "(c) 2026, Bare7a"
diff --git a/build/windows/wails.exe.manifest b/build/windows/wails.exe.manifest
index d54088f..def9f79 100644
--- a/build/windows/wails.exe.manifest
+++ b/build/windows/wails.exe.manifest
@@ -1,6 +1,6 @@
-
+
diff --git a/frontend/bench/sqlPerf.bench.test.ts b/frontend/bench/sqlPerf.bench.test.ts
new file mode 100644
index 0000000..e1d1226
--- /dev/null
+++ b/frontend/bench/sqlPerf.bench.test.ts
@@ -0,0 +1,361 @@
+/**
+ * SQL editor pipeline benchmark.
+ *
+ * Replays exactly what the editor runs at runtime, per keystroke:
+ * - run-glyphs: parseSqlStatements(fullText) (useRunGlyphs)
+ * - completion: parseSqlStatements -> currentStatementRange ->
+ * parseQueryContext(stmt) -> bindingsNeedingColumns ->
+ * buildCompletionItems -> completionReplaceRange (createSqlCompletionProvider)
+ * - diagnostics: collectSchemaDiagnostics(fullText) (useSqlDiagnostics, debounce fire)
+ * - hover: parseSqlStatements -> currentStatementRange ->
+ * parseQueryContext(stmt) -> analyzeHover (createSqlHoverProvider)
+ *
+ * Identical file is run on both branches; results go to BENCH_OUT as JSON.
+ */
+import fs from 'node:fs';
+import i18n from 'i18next';
+import { it } from 'vitest';
+import {
+ bindingsNeedingColumns,
+ buildCompletionItems,
+ completionReplaceRange,
+} from '@/features/editor/lib/sqlCompletion';
+import { collectSchemaDiagnostics } from '@/features/editor/lib/sqlDiagnostics';
+import { analyzeHover } from '@/features/editor/lib/sqlHover';
+import { parseQueryContext } from '@/features/editor/lib/sqlQueryParse';
+import { columnCacheKey } from '@/features/editor/lib/sqlQuoting';
+import { currentStatementRange, parseSqlStatements } from '@/features/editor/lib/sqlStatements';
+import type { CompletionContext } from '@/features/editor/lib/sqlSuggestions';
+import bg from '@/i18n/locales/bg.json';
+import de from '@/i18n/locales/de.json';
+import en from '@/i18n/locales/en.json';
+import type { ColumnInfo, DriverType, SchemaInfo, TableInfo } from '@/types';
+
+// Same init as src/test/setupI18n.ts (the app inits the same singleton at boot).
+if (!i18n.isInitialized) {
+ await i18n.init({
+ resources: { en: { translation: en }, de: { translation: de }, bg: { translation: bg } },
+ lng: 'en',
+ fallbackLng: 'en',
+ interpolation: { escapeValue: false },
+ returnEmptyString: false,
+ });
+}
+
+const DRIVER: DriverType = 'postgres' as DriverType;
+
+// ---------- fixture: 250 tables x 40 columns, shared id/created_at/updated_at ----------
+
+const TYPES = ['integer', 'text', 'numeric(12,2)', 'timestamptz', 'boolean', 'uuid', 'jsonb', 'bigint'];
+
+function makeColumns(table: string): ColumnInfo[] {
+ const cols: ColumnInfo[] = [
+ { name: 'id', dataType: 'integer', isNullable: false, isPrimary: true, isForeign: false },
+ { name: 'created_at', dataType: 'timestamptz', isNullable: false, isPrimary: false, isForeign: false },
+ { name: 'updated_at', dataType: 'timestamptz', isNullable: true, isPrimary: false, isForeign: false },
+ ];
+ for (let i = 0; i < 37; i++) {
+ cols.push({
+ name: `${table}_c${i}`,
+ dataType: TYPES[i % TYPES.length],
+ isNullable: i % 3 !== 0,
+ isPrimary: false,
+ isForeign: i === 5,
+ foreignTable: i === 5 ? 'users' : undefined,
+ foreignColumn: i === 5 ? 'id' : undefined,
+ });
+ }
+ return cols;
+}
+
+const NAMED = ['users', 'orders', 'order_items', 'products', 'customers'];
+const tables: TableInfo[] = [];
+for (const name of NAMED) tables.push({ schema: 'public', name, type: 'table' });
+for (let i = NAMED.length; i < 250; i++)
+ tables.push({ schema: 'public', name: `t_${String(i).padStart(3, '0')}`, type: i % 10 === 0 ? 'view' : 'table' });
+
+const schemas: SchemaInfo[] = [{ name: 'public' }, { name: 'analytics' }, { name: 'audit' }];
+const tablesBySchema: Record = { public: tables, analytics: [], audit: [] };
+
+const columnsByTable: Record = {};
+for (const tbl of tables) columnsByTable[columnCacheKey(tbl.schema, tbl.name)] = makeColumns(tbl.name);
+// Domain columns used by the typed queries.
+columnsByTable['public.users'].push(
+ { name: 'email', dataType: 'text', isNullable: false, isPrimary: false, isForeign: false },
+ { name: 'full_name', dataType: 'text', isNullable: true, isPrimary: false, isForeign: false },
+);
+columnsByTable['public.orders'].push(
+ { name: 'total', dataType: 'numeric(12,2)', isNullable: false, isPrimary: false, isForeign: false },
+ {
+ name: 'user_id',
+ dataType: 'integer',
+ isNullable: false,
+ isPrimary: false,
+ isForeign: true,
+ foreignTable: 'users',
+ foreignColumn: 'id',
+ },
+ {
+ name: 'customer_id',
+ dataType: 'integer',
+ isNullable: false,
+ isPrimary: false,
+ isForeign: true,
+ foreignTable: 'customers',
+ foreignColumn: 'id',
+ },
+);
+columnsByTable['public.order_items'].push(
+ {
+ name: 'order_id',
+ dataType: 'integer',
+ isNullable: false,
+ isPrimary: false,
+ isForeign: true,
+ foreignTable: 'orders',
+ foreignColumn: 'id',
+ },
+ {
+ name: 'product_id',
+ dataType: 'integer',
+ isNullable: false,
+ isPrimary: false,
+ isForeign: true,
+ foreignTable: 'products',
+ foreignColumn: 'id',
+ },
+);
+
+const ctx: CompletionContext = { schemas, tables, columns: [], tablesBySchema, columnsByTable, driver: DRIVER };
+
+// ---------- measurement plumbing ----------
+
+interface Stats {
+ n: number;
+ meanMs: number;
+ p50Ms: number;
+ p95Ms: number;
+ maxMs: number;
+ totalMs: number;
+}
+
+function stats(samples: number[]): Stats {
+ const s = [...samples].sort((a, b) => a - b);
+ const total = s.reduce((a, b) => a + b, 0);
+ const q = (p: number) => s[Math.min(s.length - 1, Math.floor(p * s.length))] ?? 0;
+ return {
+ n: s.length,
+ meanMs: total / (s.length || 1),
+ p50Ms: q(0.5),
+ p95Ms: q(0.95),
+ maxMs: s[s.length - 1] ?? 0,
+ totalMs: total,
+ };
+}
+
+let peakRss = 0;
+function sampleRss(): void {
+ const rss = process.memoryUsage().rss;
+ if (rss > peakRss) peakRss = rss;
+}
+
+function gcNow(): void {
+ (globalThis as { gc?: () => void }).gc?.();
+}
+
+// ---------- editor pipeline replicas ----------
+
+function glyphPass(text: string): number {
+ return parseSqlStatements(text, DRIVER).length;
+}
+
+function completionRequest(text: string, offset: number): number {
+ const { start, end } = currentStatementRange(parseSqlStatements(text, DRIVER), offset, text.length);
+ const textBefore = text.slice(start, offset);
+ const parsed = parseQueryContext(text.slice(start, end), tables, schemas, DRIVER);
+ bindingsNeedingColumns(textBefore, parsed, { tables, schemas, driver: DRIVER });
+ const items = buildCompletionItems({ ctx, text, position: offset, parsed, statementStart: start });
+ completionReplaceRange(
+ { lineNumber: 1, column: offset + 1 },
+ textBefore,
+ { startColumn: offset + 1, endColumn: offset + 1 },
+ DRIVER,
+ );
+ return items.length;
+}
+
+function diagnosticsFire(text: string): number {
+ return collectSchemaDiagnostics(text, tables, schemas, DRIVER).length;
+}
+
+function hoverRequest(text: string, offset: number): boolean {
+ const { start, end } = currentStatementRange(parseSqlStatements(text, DRIVER), offset, text.length);
+ const stmt = text.slice(start, end);
+ const parsed = parseQueryContext(stmt, tables, schemas, DRIVER);
+ return analyzeHover(stmt, offset - start, parsed, tables, schemas, DRIVER) !== null;
+}
+
+// ---------- workloads ----------
+
+interface WorkloadResult {
+ buckets: Record;
+ wallMs: number;
+ cpuUserMs: number;
+ cpuSysMs: number;
+ checksum: number;
+}
+
+function runWorkload(fn: (push: (bucket: string, ms: number) => void) => number): WorkloadResult {
+ const buckets: Record = {};
+ const push = (bucket: string, ms: number) => {
+ buckets[bucket] ??= [];
+ buckets[bucket].push(ms);
+ };
+ gcNow();
+ const cpu0 = process.cpuUsage();
+ const t0 = performance.now();
+ const checksum = fn(push);
+ const wallMs = performance.now() - t0;
+ const cpu = process.cpuUsage(cpu0);
+ const out: Record = {};
+ for (const [k, v] of Object.entries(buckets)) out[k] = stats(v);
+ return { buckets: out, wallMs, cpuUserMs: cpu.user / 1000, cpuSysMs: cpu.system / 1000, checksum };
+}
+
+function typedQuery(salt: string): string {
+ return (
+ `SELECT u.id, u.email, o.total FROM users u JOIN orders o ON o.user_id = u.id ` +
+ `WHERE u.email LIKE 'a${salt}%' AND o.total > 100 ORDER BY o.created_at DESC`
+ );
+}
+
+// Simulates typing `typed` at the end of `prefix`, running the per-keystroke pipeline.
+function typingSession(prefix: string, typed: string, push: (b: string, ms: number) => void, diagEvery = 12): number {
+ let checksum = 0;
+ for (let k = 1; k <= typed.length; k++) {
+ const text = prefix + typed.slice(0, k);
+ const offset = text.length;
+
+ let t0 = performance.now();
+ checksum += glyphPass(text);
+ push('glyphs', performance.now() - t0);
+
+ t0 = performance.now();
+ checksum += completionRequest(text, offset);
+ push('completion', performance.now() - t0);
+
+ if (k % diagEvery === 0 || k === typed.length) {
+ t0 = performance.now();
+ checksum += diagnosticsFire(text);
+ push('diagnostics', performance.now() - t0);
+ }
+ sampleRss();
+ }
+ return checksum;
+}
+
+const W1_SESSIONS = 8;
+function w1(push: (b: string, ms: number) => void): number {
+ let checksum = 0;
+ for (let s = 0; s < W1_SESSIONS; s++) checksum += typingSession('', typedQuery(`s${s}`), push);
+ return checksum;
+}
+
+const W2_SESSIONS = 6;
+const W2_STATEMENTS = 150;
+function bigFilePrefix(salt: string): string {
+ const lines: string[] = [];
+ for (let i = 0; i < W2_STATEMENTS; i++) {
+ const t = `t_${String((i % 245) + 5).padStart(3, '0')}`;
+ lines.push(`SELECT id, created_at, ${t}_c${i % 37} FROM ${t} WHERE ${t}_c3 = ${i} /*${salt}*/;`);
+ }
+ return `${lines.join('\n')}\n`;
+}
+function w2(push: (b: string, ms: number) => void): number {
+ let checksum = 0;
+ for (let s = 0; s < W2_SESSIONS; s++) checksum += typingSession(bigFilePrefix(`s${s}`), typedQuery(`s${s}`), push);
+ return checksum;
+}
+
+const W3_SESSIONS = 8;
+function w3(push: (b: string, ms: number) => void): number {
+ let checksum = 0;
+ for (let s = 0; s < W3_SESSIONS; s++) {
+ const fixed =
+ `/*s${s}*/ SELECT * FROM users u JOIN orders o ON o.user_id = u.id ` +
+ `JOIN order_items oi ON oi.order_id = o.id JOIN products p ON p.id = oi.product_id ` +
+ `JOIN customers c ON c.id = o.customer_id WHERE `;
+ const typedTail = 'created_at > now() AND u.email = c.customers_c1';
+ checksum += typingSession('', fixed, push, 1_000_000); // no diag churn here
+ for (let k = 1; k <= typedTail.length; k++) {
+ const text = fixed + typedTail.slice(0, k);
+ const t0 = performance.now();
+ checksum += completionRequest(text, text.length);
+ push('completion-wide', performance.now() - t0);
+ sampleRss();
+ }
+ }
+ return checksum;
+}
+
+const W4_SESSIONS = 6;
+const W4_SWEEPS = 40;
+function w4(push: (b: string, ms: number) => void): number {
+ let checksum = 0;
+ for (let s = 0; s < W4_SESSIONS; s++) {
+ const text = typedQuery(`s${s}`);
+ for (let sweep = 0; sweep < W4_SWEEPS; sweep++) {
+ for (let offset = 0; offset < text.length; offset += 3) {
+ const t0 = performance.now();
+ checksum += hoverRequest(text, offset) ? 1 : 0;
+ push('hover', performance.now() - t0);
+ }
+ sampleRss();
+ }
+ }
+ return checksum;
+}
+
+// ---------- main ----------
+
+it('sql editor pipeline benchmark', () => {
+ // JIT warmup on salted inputs that are never measured.
+ {
+ const sink: string[] = [];
+ typingSession('', typedQuery('warm'), () => sink.push(''), 10);
+ typingSession(bigFilePrefix('warm'), typedQuery('warm'), () => sink.push(''), 10);
+ }
+ gcNow();
+ const baselineHeap = process.memoryUsage().heapUsed;
+ peakRss = 0;
+ sampleRss();
+
+ const results: Record = {};
+ results.typing_small = runWorkload(w1);
+ results.typing_bigfile = runWorkload(w2);
+ results.completion_wide = runWorkload(w3);
+ results.hover_sweep = runWorkload(w4);
+
+ const finalRssBeforeGc = process.memoryUsage().rss;
+ gcNow();
+ const retainedHeap = process.memoryUsage().heapUsed - baselineHeap;
+
+ const out = {
+ node: process.version,
+ side: process.env.BENCH_SIDE ?? 'unknown',
+ run: Number(process.env.BENCH_RUN ?? 0),
+ workloads: results,
+ memory: {
+ baselineHeapMB: baselineHeap / 1048576,
+ retainedHeapMB: retainedHeap / 1048576,
+ peakRssMB: peakRss / 1048576,
+ finalRssMB: finalRssBeforeGc / 1048576,
+ // darwin reports maxRSS in bytes, linux in KiB.
+ maxRssMB: process.resourceUsage().maxRSS / (process.platform === 'darwin' ? 1048576 : 1024),
+ },
+ };
+
+ const dest = process.env.BENCH_OUT;
+ if (dest) fs.writeFileSync(dest, JSON.stringify(out, null, 2));
+ console.log(JSON.stringify(out));
+}, 900_000);
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 1275739..352c9a4 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -1,35 +1,35 @@
{
"name": "xensql-frontend",
- "version": "1.4.2",
+ "version": "1.4.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "xensql-frontend",
- "version": "1.4.2",
+ "version": "1.4.3",
"dependencies": {
- "@fontsource/fira-code": "^5.2.7",
- "@fontsource/inter": "^5.2.8",
+ "@fontsource/fira-code": "^5.3.0",
+ "@fontsource/inter": "^5.3.0",
"@monaco-editor/react": "^4.7.0",
- "@tanstack/react-virtual": "^3.14.5",
+ "@tanstack/react-virtual": "^3.14.7",
"@wailsio/runtime": "latest",
- "i18next": "^26.3.4",
- "lucide-react": "^1.23.0",
- "monaco-editor": "^0.55.1",
- "react": "^19.2.7",
- "react-dom": "^19.2.7",
- "react-i18next": "^17.0.8",
+ "i18next": "^26.3.6",
+ "lucide-react": "^1.25.0",
+ "monaco-editor": "^0.56.0",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8",
+ "react-i18next": "^17.0.10",
"sql-formatter": "^15.8.2",
"zustand": "^5.0.14"
},
"devDependencies": {
- "@biomejs/biome": "2.5.2",
- "@types/node": "^26.1.0",
+ "@biomejs/biome": "2.5.5",
+ "@types/node": "^26.1.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
- "@vitejs/plugin-react": "^6.0.3",
- "typescript": "^6.0.3",
- "vite": "^8.1.3",
+ "@vitejs/plugin-react": "^6.0.4",
+ "typescript": "^7.0.2",
+ "vite": "^8.1.5",
"vitest": "^4.1.10"
}
},
@@ -43,9 +43,9 @@
}
},
"node_modules/@biomejs/biome": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.2.tgz",
- "integrity": "sha512-VQ3RCqr7JmDIX+w6stWYl+g/3bYofN3q2wDBHUKKc/c7i5QWrFKFBZYCYPWTE6agsUPMIZZe6/CMmVUfUAhkKA==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.5.5.tgz",
+ "integrity": "sha512-r1S8nFsAG1MY+vJFZALzIvwXAJv6ejDQ0mxP21Tgr9YK3ZFtjrvbBwDdNhx1rUqvccEIeNg20cYCNzl6Cr69pQ==",
"dev": true,
"license": "MIT OR Apache-2.0",
"bin": {
@@ -59,20 +59,20 @@
"url": "https://opencollective.com/biome"
},
"optionalDependencies": {
- "@biomejs/cli-darwin-arm64": "2.5.2",
- "@biomejs/cli-darwin-x64": "2.5.2",
- "@biomejs/cli-linux-arm64": "2.5.2",
- "@biomejs/cli-linux-arm64-musl": "2.5.2",
- "@biomejs/cli-linux-x64": "2.5.2",
- "@biomejs/cli-linux-x64-musl": "2.5.2",
- "@biomejs/cli-win32-arm64": "2.5.2",
- "@biomejs/cli-win32-x64": "2.5.2"
+ "@biomejs/cli-darwin-arm64": "2.5.5",
+ "@biomejs/cli-darwin-x64": "2.5.5",
+ "@biomejs/cli-linux-arm64": "2.5.5",
+ "@biomejs/cli-linux-arm64-musl": "2.5.5",
+ "@biomejs/cli-linux-x64": "2.5.5",
+ "@biomejs/cli-linux-x64-musl": "2.5.5",
+ "@biomejs/cli-win32-arm64": "2.5.5",
+ "@biomejs/cli-win32-x64": "2.5.5"
}
},
"node_modules/@biomejs/cli-darwin-arm64": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.2.tgz",
- "integrity": "sha512-e7P3P7EkwFc/KiX2AHw4YDLIBOMfG9CPCAwy52k5Bp0dfhkozx9hf6wCmIr2QeXy2XeccJ3V/Sg+hDmzYEqxSg==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.5.5.tgz",
+ "integrity": "sha512-kUrAhXVWUrwmAUnV2iXSK7umxKFysTwvqK+Ty6ptUcLY/7T3SnCAjUowE4uvwaEej6nXZ7hu/dTtbokKdsPeag==",
"cpu": [
"arm64"
],
@@ -87,9 +87,9 @@
}
},
"node_modules/@biomejs/cli-darwin-x64": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.2.tgz",
- "integrity": "sha512-ymzMvjC1Jg0b9K0D26ZdARqFQXs7MocfLC5FOCGfkC0Ss+ACUJkX5364ZM5nT4NLZanHRZNVrZEy+Ibwcvux/g==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.5.5.tgz",
+ "integrity": "sha512-DamiYc5bUYZ2uxlfc+RLEPtz1Abb6PO5eTbOkufLpSGwd/7AMQAdxhFYiXmwwkJL8IsT8S7GvdgwDHqaMFAvKw==",
"cpu": [
"x64"
],
@@ -104,9 +104,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.2.tgz",
- "integrity": "sha512-t7sseOmqND57uUWTwlawU6BYj+J06T/9EkydzBhkrgw/FK3QVhjU2wsJR0frljrKZ0/I8A/rYw7284QgqjQfIQ==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.5.5.tgz",
+ "integrity": "sha512-lRKF/pH/1RiYiBKExi3TCZVAtvzEm77aifrvcNiDFrR9WxeAnDUjDnseb6y2XV85mjitLs6SILGm2XG77cHtSQ==",
"cpu": [
"arm64"
],
@@ -124,9 +124,9 @@
}
},
"node_modules/@biomejs/cli-linux-arm64-musl": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.2.tgz",
- "integrity": "sha512-w+ANG0ZvTu9IeEg9QnstoOnk6L0fpwJifW6aHR18+cb5Z39bkANItYjAfMrnvce5tmMK+IQ6nPX7/kQFdam5iw==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.5.5.tgz",
+ "integrity": "sha512-U4WMl/sy/E/Q73vf15VspakLRRs2LDFcCeBxJnQfXzssb88zpV6PJPaQ3ezhQ7H6Ht2/8bvuZeHgJWzmoxllZg==",
"cpu": [
"arm64"
],
@@ -144,9 +144,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.2.tgz",
- "integrity": "sha512-M/lOZrewzTCRDINbjhQ1gYYru37KlD3kJBQwwKCG0ckz5E9IZwIoJ3X0wBwRXA+yBDIwWUuPBHS67HzJY4dTfA==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.5.5.tgz",
+ "integrity": "sha512-H/O39nJEw/2Zm/fm7hrmxxoF8kK/aU1uCoPp70ruXVbomaAdLpJJnCmL11Q2JotT8QVHH06So04Oq53lCSwSwQ==",
"cpu": [
"x64"
],
@@ -164,9 +164,9 @@
}
},
"node_modules/@biomejs/cli-linux-x64-musl": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.2.tgz",
- "integrity": "sha512-VArNLAzND063tF+XY0yPyM+DyahpzOMzOAvb7qs259nhjJWRjvjZdssuA+Rfl+l07+NOesKZ0Xu2yFrXyBMtzw==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.5.5.tgz",
+ "integrity": "sha512-m7wC7tjX5Lrmo69dc4md8FeKpPU1NTCY1v7xUoQQ2vadWwNnBS0KZOG8471otFPHrTHihQJAjQPgMObpLvDe6A==",
"cpu": [
"x64"
],
@@ -184,9 +184,9 @@
}
},
"node_modules/@biomejs/cli-win32-arm64": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.2.tgz",
- "integrity": "sha512-kbjFFKyZlzYnAuw7sRy5qDoFG6zrP40UK08oPQsWK0ct3NMnGSt+Bs1iviEEyEIP57N5MrykGXdO/wRiaR4lww==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.5.5.tgz",
+ "integrity": "sha512-7BryINPuYypLUAH3o/o5ZdgomJ4zn3EDR0ChZJst7n32S6ZhKbgHXuYydLu+YAnx59ehGFR0z/MG6qnzQi3Yyw==",
"cpu": [
"arm64"
],
@@ -201,9 +201,9 @@
}
},
"node_modules/@biomejs/cli-win32-x64": {
- "version": "2.5.2",
- "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.2.tgz",
- "integrity": "sha512-4InchVpdVmdkkkgjQqKpgvyu+VPnoF/7RPSw5YATgEVpt2j72wcCAeV5TwaE9ZGJUZWZn7v2CwSAj6CrMJEx8A==",
+ "version": "2.5.5",
+ "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.5.5.tgz",
+ "integrity": "sha512-bIBFo+n6MIxdNcVFy5CrurbKiZQiUciK3bt8+O9I4wjFZNTfXLpi+giq47522eXqW5NBc9ulx7dR1SlZKi2J5g==",
"cpu": [
"x64"
],
@@ -252,18 +252,18 @@
}
},
"node_modules/@fontsource/fira-code": {
- "version": "5.2.7",
- "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.2.7.tgz",
- "integrity": "sha512-tnB9NNund9TwIym8/7DMJe573nlPEQb+fKUV5GL8TBYXjIhDvL0D7mgmNVNQUPhXp+R7RylQeiBdkA4EbOHPGQ==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/fira-code/-/fira-code-5.3.0.tgz",
+ "integrity": "sha512-EJL968RJRkakubAj/coU8pSUaeTE5UNoRjtzAr6kGiSZ3jWuN8/AKWHwym/PFUaQL1q7IL/H+EXs4358YhrTBQ==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
}
},
"node_modules/@fontsource/inter": {
- "version": "5.2.8",
- "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.2.8.tgz",
- "integrity": "sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==",
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.3.0.tgz",
+ "integrity": "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g==",
"license": "OFL-1.1",
"funding": {
"url": "https://github.com/sponsors/ayuhito"
@@ -319,9 +319,9 @@
}
},
"node_modules/@oxc-project/types": {
- "version": "0.138.0",
- "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.138.0.tgz",
- "integrity": "sha512-1a7ZKmrRTCoN1XMZ4L0PyyqrMnrNlLyPuOkdSX2MZg7IiIGRUyurNhAm73ptDOraoBcIordsIGKNPKUzy3ZmfA==",
+ "version": "0.139.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz",
+ "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==",
"dev": true,
"license": "MIT",
"funding": {
@@ -329,9 +329,9 @@
}
},
"node_modules/@rolldown/binding-android-arm64": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.4.tgz",
- "integrity": "sha512-EZLpf/8y7GXkkra90ML47kzik/GMP3EMcE9bPyHmRfxLC6z9+aW5A8poCsoxjrT5GfEcNAAvWwUHjvP1pUQkfw==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz",
+ "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==",
"cpu": [
"arm64"
],
@@ -346,9 +346,9 @@
}
},
"node_modules/@rolldown/binding-darwin-arm64": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.4.tgz",
- "integrity": "sha512-aUi+HBvmYb7j8krl1+qJgkG8C17fO79gk3c+jPw4S8glRFc1DTija9S3EyaTSQUm5GJXYKDAsugBEhFHH2vYiQ==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz",
+ "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==",
"cpu": [
"arm64"
],
@@ -363,9 +363,9 @@
}
},
"node_modules/@rolldown/binding-darwin-x64": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.4.tgz",
- "integrity": "sha512-F7hHC3gwY11+vByKPRWqwGbeXWVgKmL+pTGCinaEhdihzBV2aQ0fvZOch9cXYUOKuKKq429HeYXOqQLc7wFCEg==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz",
+ "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==",
"cpu": [
"x64"
],
@@ -380,9 +380,9 @@
}
},
"node_modules/@rolldown/binding-freebsd-x64": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.4.tgz",
- "integrity": "sha512-sI5yw+7s92SK6odiEhD5lKCBlWcpjHS5qyqpVQbZAJ0fIzEUXrmbl3DH2ybR3PZogulNJF+COLtmA8hUfvkCCQ==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz",
+ "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==",
"cpu": [
"x64"
],
@@ -397,9 +397,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm-gnueabihf": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.4.tgz",
- "integrity": "sha512-mCi0OKgEieFircrtVYmQAFGszRtMnZ6fpZAXrxanXAu7lqZcsK1E1RAaZNG0uKAnxox3B1f4EyQNnoyMfN1vAA==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz",
+ "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==",
"cpu": [
"arm"
],
@@ -414,9 +414,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-gnu": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.4.tgz",
- "integrity": "sha512-B9Ial3Kv5sh0SHnB1g/QWcUQCEvCF6QKGAl4zXypYj65mVI+B4AhFBwPtSN7pDrJeIx8Z7zdy4ntx+wQABom7w==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz",
+ "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==",
"cpu": [
"arm64"
],
@@ -434,9 +434,9 @@
}
},
"node_modules/@rolldown/binding-linux-arm64-musl": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.4.tgz",
- "integrity": "sha512-lZVym0PuHE1KZ22gmFTC15lAkrg9iTszR617oYRB/iPY1A56ywoJzVKOJBKaot5RiikCObmur6pogpse3gRcng==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz",
+ "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==",
"cpu": [
"arm64"
],
@@ -454,9 +454,9 @@
}
},
"node_modules/@rolldown/binding-linux-ppc64-gnu": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.4.tgz",
- "integrity": "sha512-t2DNiLJWNTbnEHyUzTumldML6ET4/g16467LZoDDJ3tSxGvguL5/NyC2lCsNKuyRycg9XeDQF5SSv+TNOhQEXg==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz",
+ "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==",
"cpu": [
"ppc64"
],
@@ -474,9 +474,9 @@
}
},
"node_modules/@rolldown/binding-linux-s390x-gnu": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.4.tgz",
- "integrity": "sha512-0WIRnL1Uw4BvTZRLQt+PVgo6ZKTJadlC2btP+/EOXv2f/DWbY0rEgl+y834mIVwP1FkTlWVTrGGJXf12lru7EQ==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz",
+ "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==",
"cpu": [
"s390x"
],
@@ -494,9 +494,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-gnu": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.4.tgz",
- "integrity": "sha512-JWtGshGfX+oENAKonoNkqEJX+7hC8yfhi9GUyPX1VX4mdh1y5r+ZiJLR5XzAB0aoP6s/PcILsGjKq8O0mm24bw==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz",
+ "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==",
"cpu": [
"x64"
],
@@ -514,9 +514,9 @@
}
},
"node_modules/@rolldown/binding-linux-x64-musl": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.4.tgz",
- "integrity": "sha512-rT6yQcxUuXs4CnbofqwHRRV0iem349rLMYpTjkgQGLjrY4ado/eDzwPZPTCgTOlF6Nkp8NEv70yLMTn6qkWxsQ==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz",
+ "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==",
"cpu": [
"x64"
],
@@ -534,9 +534,9 @@
}
},
"node_modules/@rolldown/binding-openharmony-arm64": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.4.tgz",
- "integrity": "sha512-KXMGoboq5cyaCQjDA4GLuRiOwBQ0EyFnJoVViLeZ45/3rFItRODEr+NdsBcVpll40hhNArlm/speWGRvj08LzA==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz",
+ "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==",
"cpu": [
"arm64"
],
@@ -551,9 +551,9 @@
}
},
"node_modules/@rolldown/binding-wasm32-wasi": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.4.tgz",
- "integrity": "sha512-5K83rb36oJiY7BCyE9zLZtGcPV4g5wvq+xwdO0XPIwDVZI8cyB/AUjkNXGb92/rnmezEkjMOpgY61rtwjQtFwg==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz",
+ "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==",
"cpu": [
"wasm32"
],
@@ -570,9 +570,9 @@
}
},
"node_modules/@rolldown/binding-win32-arm64-msvc": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.4.tgz",
- "integrity": "sha512-PnWBtw3TV5KOg69HQQDR0mnQuyCmSGR2pAB4DC1rPF808fgKeTUMj2EOEyKATpgiuxuR5APQmiDO7PDgEjTFSA==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz",
+ "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==",
"cpu": [
"arm64"
],
@@ -587,9 +587,9 @@
}
},
"node_modules/@rolldown/binding-win32-x64-msvc": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.4.tgz",
- "integrity": "sha512-M1lpniBePobTfsa7Ks9a199e1akxsXn+GYBUKsEzv3YFzOm1HJAMNwKI3qr0Zq+mxwx9gOZoTdP1yXRYsZUocQ==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz",
+ "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==",
"cpu": [
"x64"
],
@@ -618,12 +618,12 @@
"license": "MIT"
},
"node_modules/@tanstack/react-virtual": {
- "version": "3.14.5",
- "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.5.tgz",
- "integrity": "sha512-4EKRXh7zBLkbKbFmG3AUVkircuHd+7OdT1pocJSepxtfBd3qnrJgJ5rtPkRYyo9fmyVb2+pI2xPy5oYvMLQy6A==",
+ "version": "3.14.7",
+ "resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.14.7.tgz",
+ "integrity": "sha512-11uSrj77IDijNBqizD4lY4y1laMyRrqMLSxjnWy5CvWkCjyRDW+gGmxYq0lwQKVas/sq7zyzYWXbL/BvBzR32g==",
"license": "MIT",
"dependencies": {
- "@tanstack/virtual-core": "3.17.3"
+ "@tanstack/virtual-core": "3.17.5"
},
"funding": {
"type": "github",
@@ -635,9 +635,9 @@
}
},
"node_modules/@tanstack/virtual-core": {
- "version": "3.17.3",
- "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.3.tgz",
- "integrity": "sha512-8Np/TFELpI0ySuJoVmjvOrQYXH/8sTX0Biv9szhFhY39xOdAAY+smrMxjxOum/ux3eM8MUJQsEJ0/R0UpvC8dw==",
+ "version": "3.17.5",
+ "resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.17.5.tgz",
+ "integrity": "sha512-AXfBC3sq6PuYSwyxYORqqgHCNjPGAvKJvZuBBJ1klhztWBB5cgqgwsq8+fNfaQJG7/K4xYBja9S90QFn2zmQAg==",
"license": "MIT",
"funding": {
"type": "github",
@@ -681,9 +681,9 @@
"license": "MIT"
},
"node_modules/@types/node": {
- "version": "26.1.0",
- "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.0.tgz",
- "integrity": "sha512-O0A1G3xPGy4w7AgQdAQYUlQ+BKk2Oovw8eRpofyp5KdBZULnbe+WqaOVNrm705SHphCiG4XHsACrSmPu1f+Kgw==",
+ "version": "26.1.1",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz",
+ "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -717,10 +717,330 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@typescript/typescript-aix-ppc64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-aix-ppc64/-/typescript-aix-ppc64-7.0.2.tgz",
+ "integrity": "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-darwin-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-arm64/-/typescript-darwin-arm64-7.0.2.tgz",
+ "integrity": "sha512-gowzar9MwS/aRWp6f3a4KUqzRjAZjOsmGNCM6LcTgXum+dBfgsBVMN+AgvOCCbguXyick6LJhpBszxMebJ8syA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-darwin-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-darwin-x64/-/typescript-darwin-x64-7.0.2.tgz",
+ "integrity": "sha512-SZ9xZInqApNlNGc9s0W1VSsktYSOe9cFqNOIqmN1Gs8SmkjKZYFt017G4VwPxASInODuAdbTW7sXiFUf893RgA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-freebsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-arm64/-/typescript-freebsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-W5NH4y/J0plIIS5b2xvTEkU7JFxyqdMAOgf+Ilhl0vHQXKO5dZoxd+C/jEtq56c4F3wk71RB4BMRQ2XdI+bwYQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-freebsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-freebsd-x64/-/typescript-freebsd-x64-7.0.2.tgz",
+ "integrity": "sha512-UMGDx5sTpzNw3WiPebH7l90IWfJggEd+egHt/q6p7/Cm3zqoV7VxkGXt+3DxPIw8CcmvAB0j3sVVfbhX+M4Tpw==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-arm": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm/-/typescript-linux-arm-7.0.2.tgz",
+ "integrity": "sha512-gffT3xPz9sR7j/YJExkyPntrI0P2EP9XbOyWzth2/Gs0RstK+90RBcO0ncXoXy/beYll1SXw846Nf2zdnEz0QQ==",
+ "cpu": [
+ "arm"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-arm64/-/typescript-linux-arm64-7.0.2.tgz",
+ "integrity": "sha512-Qh4eU4/y3yDjnfjjyPYihMj5/ODIlmt+Bzu17OI+fiSRDW57QmU5SiN63exPRNJPKUzcc1INa1NXdrJ+MqHjUQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-loong64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-loong64/-/typescript-linux-loong64-7.0.2.tgz",
+ "integrity": "sha512-uEHck9i8hoAzXPiYRib1O7miOnz23SxIeVl6F4LXox+qov1K35jHcEW6VHKvZI+pyvl7fZEP4MCU5LYvIq1GuQ==",
+ "cpu": [
+ "loong64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-mips64el": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-mips64el/-/typescript-linux-mips64el-7.0.2.tgz",
+ "integrity": "sha512-R4KvAMnE43W5Qeqb0Ly56O3mWMWIAgsMyz36DCaycd5nbg/9kzm0liw3JocfRqyJY0KPmzFjbswozXyW0DnIYA==",
+ "cpu": [
+ "mips64el"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-ppc64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-ppc64/-/typescript-linux-ppc64-7.0.2.tgz",
+ "integrity": "sha512-DORx5b3sd/4S7eayxm4FQv+A7CrkUIGRaHiwI8oiHTAI1fAPWhF4J0vAlkC8biAlHSVVwxMQ3tjZ2/DVbnQiiA==",
+ "cpu": [
+ "ppc64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-riscv64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-riscv64/-/typescript-linux-riscv64-7.0.2.tgz",
+ "integrity": "sha512-wf0jqEDOjrPRnKwYRyyJDRo11KMbvMFrU+q4zqKyChODBzvlkbhNQfKvLxQCcwTpdDaXSHZTVuh0JoCrKCUMHQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-s390x": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-s390x/-/typescript-linux-s390x-7.0.2.tgz",
+ "integrity": "sha512-IkwJc3L7yhytWd/ewjyxNDfOmswCm9GWMJT/ue/dU4aZNbwZeYAetq42VyLmsmSjvoX7z74X6ZaYCtzAr0EuGw==",
+ "cpu": [
+ "s390x"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-linux-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-linux-x64/-/typescript-linux-x64-7.0.2.tgz",
+ "integrity": "sha512-EYdf2cNg7rgCWJnxCdJ+F3V39O8ihb37eHAu1LK8oAFizgTQbPOK7zHHXbPt8rX24COqODXeI3sIf0fCXG7H/A==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-netbsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-arm64/-/typescript-netbsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-+polYF4MF04aPpO5FTkHran9yUQDSXqy5GiSDKpsll5jy3l3+g9QLhpf39T+ePtefhXLOGrLl0QIjkQP6VnelA==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-netbsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-netbsd-x64/-/typescript-netbsd-x64-7.0.2.tgz",
+ "integrity": "sha512-8YIT0EHM/3dq10ZOVF/A7pc/YSMtbcecct4rWtexrnSCHOPcpC2KTLXfTCR6vDpnSiY12heNb1GiN/wu+T/FyA==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-openbsd-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-arm64/-/typescript-openbsd-arm64-7.0.2.tgz",
+ "integrity": "sha512-APT8+ClYnuYm1u9+kgGXoMj2VzWzcymwh2gNSQVySHfkRDGOTVkoWLjCmOQSaO+PoqQ57B0flRp9SA+7GnnkzQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-openbsd-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-openbsd-x64/-/typescript-openbsd-x64-7.0.2.tgz",
+ "integrity": "sha512-yX7s+Q0Dln0Dt9tEzZsAjXXR/+ytBM7AlglaqyeMPxQszJ1JhlJdZ6jLA+IzldHtflX81em7lDao1xXu+aRRkg==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-sunos-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-sunos-x64/-/typescript-sunos-x64-7.0.2.tgz",
+ "integrity": "sha512-dLJDGaLZ1D4HPQn62u1n8mBDkJREwMsAkCdkwd4Ieqw+x3TUyTsqY0YiBCtE6H6OzzgGk3iuZ3vFWRS+E8/d1g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-win32-arm64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-arm64/-/typescript-win32-arm64-7.0.2.tgz",
+ "integrity": "sha512-Gyl1Vy6OsWesLzmq+EP0Fb7b4Nid5232AvcA2SFcdYreldpNtYFFofPjnt62y9hQy7VTaZp65ICJjuAQRaVcIQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
+ "node_modules/@typescript/typescript-win32-x64": {
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/@typescript/typescript-win32-x64/-/typescript-win32-x64-7.0.2.tgz",
+ "integrity": "sha512-0BQ3HkAHHlKLSp1qRvf3SUhGpGsDuhB/jgFw75guyqbxJqEaS0Cw/VFO8i2nHglJUzQCRtMMR/IBAKE3ETMC4g==",
+ "cpu": [
+ "x64"
+ ],
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=16.20.0"
+ }
+ },
"node_modules/@vitejs/plugin-react": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz",
- "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==",
+ "version": "6.0.4",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.4.tgz",
+ "integrity": "sha512-XcCQz0TBpBgljhj0gMuuDj49i6Ytqh5q1osT/Gp5uAVJUCTWxyskk/l1jwYYiu2xcNHHipdMz40EGfM1VdamVg==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -925,9 +1245,9 @@
"license": "MIT"
},
"node_modules/dompurify": {
- "version": "3.4.11",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
- "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
+ "version": "3.4.12",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
+ "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -1003,9 +1323,9 @@
}
},
"node_modules/i18next": {
- "version": "26.3.4",
- "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.4.tgz",
- "integrity": "sha512-pa7m0d7pBDqGHZxljT+WPFeyFgQ7P7SciPPo1tTqYuO0z4sqADYhwnBESmmGp/wEof1inwdls/k8ZgTg8rxFHA==",
+ "version": "26.3.6",
+ "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.3.6.tgz",
+ "integrity": "sha512-Bu5Z2nAXgfVyM8xvW3jk9EKRIuX37PudsrBViThNFx7CR7aaYTpP01cxNB/E4c4UUzTDiAZRstEhsRfPOL/8xA==",
"funding": [
{
"type": "individual",
@@ -1022,7 +1342,7 @@
],
"license": "MIT",
"peerDependencies": {
- "typescript": "^5 || ^6"
+ "typescript": "^5 || ^6 || ^7"
},
"peerDependenciesMeta": {
"typescript": {
@@ -1292,9 +1612,9 @@
}
},
"node_modules/lucide-react": {
- "version": "1.23.0",
- "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.23.0.tgz",
- "integrity": "sha512-38BpJcD0JhFosxHApP/BYsBetLpQFRoTRzEzstM/XCc3jsAG7wqaY1lgVwxiUe3xqYE+lNxo2PkCmYwXWrwwIw==",
+ "version": "1.25.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.25.0.tgz",
+ "integrity": "sha512-/mdJTRbiwcLOQ1NZZK1amZF9rIZyvO18D6r9TngE6TG1NmqHgFuT4eE7Xrkm9UsXMbBJD1NlfwHVltCDWHrOTw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
@@ -1323,12 +1643,12 @@
}
},
"node_modules/monaco-editor": {
- "version": "0.55.1",
- "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.55.1.tgz",
- "integrity": "sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==",
+ "version": "0.56.0",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.56.0.tgz",
+ "integrity": "sha512-sXboRm3BeBeLm938eaiyLMe0OxzfXIlZvbv4ir/jVgQy1zDhWjgmny0WoN45fuDKhCCQsYMbBJrv/A6jd8aCUg==",
"license": "MIT",
"dependencies": {
- "dompurify": "3.2.7",
+ "dompurify": "3.4.8",
"marked": "14.0.0"
}
},
@@ -1339,9 +1659,9 @@
"license": "BSD-3-Clause"
},
"node_modules/nanoid": {
- "version": "3.3.15",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
- "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"dev": true,
"funding": [
{
@@ -1405,9 +1725,9 @@
"license": "ISC"
},
"node_modules/picomatch": {
- "version": "4.0.4",
- "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz",
- "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
"dev": true,
"license": "MIT",
"engines": {
@@ -1418,9 +1738,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.16",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
- "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
+ "version": "8.5.22",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz",
+ "integrity": "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==",
"dev": true,
"funding": [
{
@@ -1438,7 +1758,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.12",
+ "nanoid": "^3.3.16",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -1466,30 +1786,30 @@
}
},
"node_modules/react": {
- "version": "19.2.7",
- "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz",
- "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==",
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz",
+ "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
- "version": "19.2.7",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz",
- "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==",
+ "version": "19.2.8",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz",
+ "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==",
"license": "MIT",
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
- "react": "^19.2.7"
+ "react": "^19.2.8"
}
},
"node_modules/react-i18next": {
- "version": "17.0.8",
- "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.8.tgz",
- "integrity": "sha512-0ooKbGLU8JXhe1zwpQUWIeXSgLPOfwJmgheWRIUpcoA0CpyabpGhayjdG+/eA5esC1AQ8h2jWpXjJfzQzeDOCw==",
+ "version": "17.0.10",
+ "resolved": "https://registry.npmjs.org/react-i18next/-/react-i18next-17.0.10.tgz",
+ "integrity": "sha512-XneHftyYA774MJkkccSkZ5oKrUpCnXIPmxio3wemqrVzCRLWiGXOMbIzObrer03fNDEnm8g8R5yYls4HcE+esg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.29.2",
@@ -1499,7 +1819,7 @@
"peerDependencies": {
"i18next": ">= 26.2.0",
"react": ">= 16.8.0",
- "typescript": "^5 || ^6"
+ "typescript": "^5 || ^6 || ^7"
},
"peerDependenciesMeta": {
"react-dom": {
@@ -1523,13 +1843,13 @@
}
},
"node_modules/rolldown": {
- "version": "1.1.4",
- "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.4.tgz",
- "integrity": "sha512-IjZYiLxZwpnhwhdBH2ugdTGVSdhCQUmLxLoqyjiL0JxYjyRst+5a0P3xfrTxJ5F638j4Mvvw5FAX5XE6eHpXbA==",
+ "version": "1.1.5",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz",
+ "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@oxc-project/types": "=0.138.0",
+ "@oxc-project/types": "=0.139.0",
"@rolldown/pluginutils": "^1.0.0"
},
"bin": {
@@ -1539,21 +1859,21 @@
"node": "^20.19.0 || >=22.12.0"
},
"optionalDependencies": {
- "@rolldown/binding-android-arm64": "1.1.4",
- "@rolldown/binding-darwin-arm64": "1.1.4",
- "@rolldown/binding-darwin-x64": "1.1.4",
- "@rolldown/binding-freebsd-x64": "1.1.4",
- "@rolldown/binding-linux-arm-gnueabihf": "1.1.4",
- "@rolldown/binding-linux-arm64-gnu": "1.1.4",
- "@rolldown/binding-linux-arm64-musl": "1.1.4",
- "@rolldown/binding-linux-ppc64-gnu": "1.1.4",
- "@rolldown/binding-linux-s390x-gnu": "1.1.4",
- "@rolldown/binding-linux-x64-gnu": "1.1.4",
- "@rolldown/binding-linux-x64-musl": "1.1.4",
- "@rolldown/binding-openharmony-arm64": "1.1.4",
- "@rolldown/binding-wasm32-wasi": "1.1.4",
- "@rolldown/binding-win32-arm64-msvc": "1.1.4",
- "@rolldown/binding-win32-x64-msvc": "1.1.4"
+ "@rolldown/binding-android-arm64": "1.1.5",
+ "@rolldown/binding-darwin-arm64": "1.1.5",
+ "@rolldown/binding-darwin-x64": "1.1.5",
+ "@rolldown/binding-freebsd-x64": "1.1.5",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.1.5",
+ "@rolldown/binding-linux-arm64-gnu": "1.1.5",
+ "@rolldown/binding-linux-arm64-musl": "1.1.5",
+ "@rolldown/binding-linux-ppc64-gnu": "1.1.5",
+ "@rolldown/binding-linux-s390x-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-gnu": "1.1.5",
+ "@rolldown/binding-linux-x64-musl": "1.1.5",
+ "@rolldown/binding-openharmony-arm64": "1.1.5",
+ "@rolldown/binding-wasm32-wasi": "1.1.5",
+ "@rolldown/binding-win32-arm64-msvc": "1.1.5",
+ "@rolldown/binding-win32-x64-msvc": "1.1.5"
}
},
"node_modules/scheduler": {
@@ -1665,17 +1985,38 @@
"optional": true
},
"node_modules/typescript": {
- "version": "6.0.3",
- "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz",
- "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==",
+ "version": "7.0.2",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-7.0.2.tgz",
+ "integrity": "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA==",
"devOptional": true,
"license": "Apache-2.0",
"bin": {
- "tsc": "bin/tsc",
- "tsserver": "bin/tsserver"
+ "tsc": "bin/tsc"
},
"engines": {
- "node": ">=14.17"
+ "node": ">=16.20.0"
+ },
+ "optionalDependencies": {
+ "@typescript/typescript-aix-ppc64": "7.0.2",
+ "@typescript/typescript-darwin-arm64": "7.0.2",
+ "@typescript/typescript-darwin-x64": "7.0.2",
+ "@typescript/typescript-freebsd-arm64": "7.0.2",
+ "@typescript/typescript-freebsd-x64": "7.0.2",
+ "@typescript/typescript-linux-arm": "7.0.2",
+ "@typescript/typescript-linux-arm64": "7.0.2",
+ "@typescript/typescript-linux-loong64": "7.0.2",
+ "@typescript/typescript-linux-mips64el": "7.0.2",
+ "@typescript/typescript-linux-ppc64": "7.0.2",
+ "@typescript/typescript-linux-riscv64": "7.0.2",
+ "@typescript/typescript-linux-s390x": "7.0.2",
+ "@typescript/typescript-linux-x64": "7.0.2",
+ "@typescript/typescript-netbsd-arm64": "7.0.2",
+ "@typescript/typescript-netbsd-x64": "7.0.2",
+ "@typescript/typescript-openbsd-arm64": "7.0.2",
+ "@typescript/typescript-openbsd-x64": "7.0.2",
+ "@typescript/typescript-sunos-x64": "7.0.2",
+ "@typescript/typescript-win32-arm64": "7.0.2",
+ "@typescript/typescript-win32-x64": "7.0.2"
}
},
"node_modules/undici-types": {
@@ -1695,16 +2036,16 @@
}
},
"node_modules/vite": {
- "version": "8.1.3",
- "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.3.tgz",
- "integrity": "sha512-Ds+gBRbj0lwRO2Y5hwnUBdxSwlAve9LeRyU4sNnAr0ewW0gWF0n5bgXgUzbgZ49MV9BVUAQUFYVcDUcilUExMA==",
+ "version": "8.1.5",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz",
+ "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"lightningcss": "^1.32.0",
- "picomatch": "^4.0.4",
- "postcss": "^8.5.16",
- "rolldown": "~1.1.3",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.17",
+ "rolldown": "~1.1.5",
"tinyglobby": "^0.2.17"
},
"bin": {
diff --git a/frontend/package.json b/frontend/package.json
index 45da949..57a81bf 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -1,7 +1,7 @@
{
"name": "xensql-frontend",
"private": true,
- "version": "1.4.2",
+ "version": "1.4.3",
"homepage": "https://xensql.bare7a.eu",
"type": "module",
"scripts": {
@@ -18,31 +18,31 @@
"check:fix": "biome check --write ."
},
"dependencies": {
- "@fontsource/inter": "^5.2.8",
- "@fontsource/fira-code": "^5.2.7",
+ "@fontsource/inter": "^5.3.0",
+ "@fontsource/fira-code": "^5.3.0",
"@monaco-editor/react": "^4.7.0",
- "@tanstack/react-virtual": "^3.14.5",
+ "@tanstack/react-virtual": "^3.14.7",
"@wailsio/runtime": "latest",
- "i18next": "^26.3.4",
- "lucide-react": "^1.23.0",
- "monaco-editor": "^0.55.1",
- "react": "^19.2.7",
- "react-dom": "^19.2.7",
- "react-i18next": "^17.0.8",
+ "i18next": "^26.3.6",
+ "lucide-react": "^1.25.0",
+ "monaco-editor": "^0.56.0",
+ "react": "^19.2.8",
+ "react-dom": "^19.2.8",
+ "react-i18next": "^17.0.10",
"sql-formatter": "^15.8.2",
"zustand": "^5.0.14"
},
"devDependencies": {
- "@biomejs/biome": "2.5.2",
- "@types/node": "^26.1.0",
+ "@biomejs/biome": "2.5.5",
+ "@types/node": "^26.1.1",
"@types/react": "^19.2.17",
"@types/react-dom": "^19.2.3",
- "@vitejs/plugin-react": "^6.0.3",
- "typescript": "^6.0.3",
- "vite": "^8.1.3",
+ "@vitejs/plugin-react": "^6.0.4",
+ "typescript": "^7.0.2",
+ "vite": "^8.1.5",
"vitest": "^4.1.10"
},
"overrides": {
- "dompurify": "^3.4.11"
+ "dompurify": "^3.4.12"
}
}
diff --git a/frontend/src/features/editor/SqlConditionInput.tsx b/frontend/src/features/editor/SqlConditionInput.tsx
index 518bc10..e44ce56 100644
--- a/frontend/src/features/editor/SqlConditionInput.tsx
+++ b/frontend/src/features/editor/SqlConditionInput.tsx
@@ -3,6 +3,7 @@ import type { editor, languages, Position } from 'monaco-editor';
import { useCallback, useEffect, useMemo, useRef } from 'react';
import { MONACO_FONT_METRICS_OPTIONS } from '@/features/editor/lib/monacoFontMetrics';
import { getMonacoThemeName, setupMonacoBeforeMount } from '@/features/editor/lib/monacoTheme';
+import { sqlLabels } from '@/features/editor/lib/sqlLabels';
import { formatSqlIdentifier } from '@/features/editor/lib/sqlQuoting';
import { matchScore, rank } from '@/features/editor/lib/sqlSuggestions';
import { useAppTheme } from '@/shared/hooks/useAppTheme';
@@ -184,7 +185,7 @@ export function SqlConditionInput({
suggestions.push({
label: col,
kind: Kind.Field,
- detail: types?.[i] || 'column',
+ detail: types?.[i] || sqlLabels().column,
insertText: formatSqlIdentifier(col, drv),
sortText: rank(0, score, col),
range,
diff --git a/frontend/src/features/editor/SqlEditor.tsx b/frontend/src/features/editor/SqlEditor.tsx
index d136b5c..3f09a6b 100644
--- a/frontend/src/features/editor/SqlEditor.tsx
+++ b/frontend/src/features/editor/SqlEditor.tsx
@@ -173,7 +173,7 @@ export const SqlEditor = memo(function SqlEditor({
}, []);
const { updateRunGlyphs, statementsRef } = useRunGlyphs(editorRef, monacoRef, sql, languageRevision, driver);
- useSqlDiagnostics(editorRef, monacoRef, sql, allTables, schemas, driver);
+ useSqlDiagnostics(editorRef, monacoRef, sql, allTables, schemas, driver, languageRevision);
const { bindEditorActions } = useEditorActions({
editorRef,
diff --git a/frontend/src/features/editor/hooks/useSqlDiagnostics.ts b/frontend/src/features/editor/hooks/useSqlDiagnostics.ts
index 37fbe38..9065c9c 100644
--- a/frontend/src/features/editor/hooks/useSqlDiagnostics.ts
+++ b/frontend/src/features/editor/hooks/useSqlDiagnostics.ts
@@ -14,6 +14,7 @@ export function useSqlDiagnostics(
tables: TableInfo[],
schemas: SchemaInfo[],
driver: DriverType,
+ languageRevision = 0,
) {
useEffect(() => {
const ed = editorRef.current;
@@ -46,5 +47,5 @@ export function useSqlDiagnostics(
clearTimeout(handle);
if (!model.isDisposed()) monaco.editor.setModelMarkers(model, SCHEMA_MARKER_OWNER, []);
};
- }, [editorRef, monacoRef, sql, tables, schemas, driver]);
+ }, [editorRef, monacoRef, sql, tables, schemas, driver, languageRevision]);
}
diff --git a/frontend/src/features/editor/lib/createSqlHoverProvider.ts b/frontend/src/features/editor/lib/createSqlHoverProvider.ts
index f8b31b3..65336d4 100644
--- a/frontend/src/features/editor/lib/createSqlHoverProvider.ts
+++ b/frontend/src/features/editor/lib/createSqlHoverProvider.ts
@@ -1,5 +1,5 @@
import type { editor, languages } from 'monaco-editor';
-import { analyzeHover, columnHoverLines } from '@/features/editor/lib/sqlHover';
+import { analyzeHover, columnHoverLines, tableColumnsMarkdown } from '@/features/editor/lib/sqlHover';
import { parseQueryContext } from '@/features/editor/lib/sqlQueryParse';
import { currentStatementRange, parseSqlStatements } from '@/features/editor/lib/sqlStatements';
import type { ColumnInfo, DriverType, SchemaInfo, TableInfo } from '@/types';
@@ -38,7 +38,12 @@ export function createSqlHoverProvider(
};
if (query.lines) {
- return { range, contents: query.lines.map((value) => ({ value })) };
+ const contents = query.lines.map((value) => ({ value }));
+ if (query.tableColumns) {
+ const cols = await onLoadColumns(query.tableColumns.schema, query.tableColumns.table).catch(() => []);
+ contents.push(...tableColumnsMarkdown(cols).map((value) => ({ value })));
+ }
+ return { range, contents };
}
if (query.columnLookup) {
for (const binding of query.columnLookup.bindings) {
diff --git a/frontend/src/features/editor/lib/monacoFontMetrics.ts b/frontend/src/features/editor/lib/monacoFontMetrics.ts
index 23c0bb0..c579b08 100644
--- a/frontend/src/features/editor/lib/monacoFontMetrics.ts
+++ b/frontend/src/features/editor/lib/monacoFontMetrics.ts
@@ -1,4 +1,4 @@
-import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+import * as monaco from 'monaco-editor/editor/editor.api';
import { MONACO_FONT_FAMILY } from '@/shared/lib/appFonts';
export const MONACO_FONT_METRICS_OPTIONS = {
diff --git a/frontend/src/features/editor/lib/monacoSetup.ts b/frontend/src/features/editor/lib/monacoSetup.ts
index 9847485..23bf16c 100644
--- a/frontend/src/features/editor/lib/monacoSetup.ts
+++ b/frontend/src/features/editor/lib/monacoSetup.ts
@@ -1,25 +1,21 @@
-// Import the bare editor API, NOT the full 'monaco-editor' - the full entry statically
-// registers every language service (TypeScript/CSS/HTML/JSON), which makes the bundler emit
-// all their (large) workers regardless of the getWorker guard below. We register only the
-// languages the app actually uses.
+// Bare editor API + only the languages/features we use. Importing `monaco-editor` would
+// pull in every language service/worker (TypeScript, CSS, …) even with the getWorker guard.
import { loader } from '@monaco-editor/react';
-import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+import * as monaco from 'monaco-editor/editor/editor.api';
+import 'monaco-editor/editor/contrib/suggest/browser/suggestController'; // autocomplete widget
+import 'monaco-editor/features/hover/register'; // SQL hover provider UI
+import 'monaco-editor/features/find/register'; // Find / Find & Replace (context menu)
+import 'monaco-editor/features/folding/register'; // JSON / cell viewer folding
import { remeasureMonacoFonts } from '@/features/editor/lib/monacoFontMetrics';
-// JSON language service (needs a worker) - used by the row/cell JSON viewers.
-import 'monaco-editor/esm/vs/language/json/monaco.contribution';
-// Syntax-highlighting-only languages (no worker): SQL editor/filter + cell viewer (xml/html).
-import 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution';
-import 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution';
-import 'monaco-editor/esm/vs/basic-languages/html/html.contribution';
-import editorWorker from 'monaco-editor/esm/vs/editor/editor.worker?worker';
-import jsonWorker from 'monaco-editor/esm/vs/language/json/json.worker?worker';
+import 'monaco-editor/languages/features/json/register'; // RowJsonViewer + cell JSON (worker)
+import 'monaco-editor/languages/definitions/sql/register'; // SQL highlighting
+import 'monaco-editor/languages/definitions/xml/register'; // cell viewer
+import 'monaco-editor/languages/definitions/html/register'; // cell viewer
+import editorWorker from 'monaco-editor/editor/editor.worker?worker';
+import jsonWorker from 'monaco-editor/languages/features/json/json.worker?worker';
-// Bundle Monaco and its workers locally and hand the instance to @monaco-editor/react,
-// so the editor never fetches from a CDN - the app works fully offline. Only the base
-// editor worker and JSON worker are bundled; the app uses SQL (custom) + JSON + plaintext,
-// plus xml/html highlighting in the cell viewer - never the TypeScript/CSS/HTML language
-// services, so their (large) workers stay out of the bundle.
+// Local bundle for @monaco-editor/react (no CDN) — editor worker + JSON worker only.
export function initMonaco(): void {
remeasureMonacoFonts();
diff --git a/frontend/src/features/editor/lib/monacoTheme.ts b/frontend/src/features/editor/lib/monacoTheme.ts
index f23cf81..a36d50c 100644
--- a/frontend/src/features/editor/lib/monacoTheme.ts
+++ b/frontend/src/features/editor/lib/monacoTheme.ts
@@ -1,6 +1,6 @@
import type { Monaco } from '@monaco-editor/react';
import type { editor } from 'monaco-editor';
-import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
+import * as monaco from 'monaco-editor/editor/editor.api';
import type { AppTheme } from '@/shared/lib/theme';
const XENSQL_MONACO_THEME_DARK = 'xensql-dark';
diff --git a/frontend/src/features/editor/lib/sqlCache.test.ts b/frontend/src/features/editor/lib/sqlCache.test.ts
new file mode 100644
index 0000000..533c9a6
--- /dev/null
+++ b/frontend/src/features/editor/lib/sqlCache.test.ts
@@ -0,0 +1,59 @@
+import { describe, expect, it } from 'vitest';
+import { LruCache } from '@/features/editor/lib/sqlCache';
+import { analyzeSqlCursor } from '@/features/editor/lib/sqlContext';
+import { parseQueryContext } from '@/features/editor/lib/sqlQueryParse';
+import { parseSqlStatements } from '@/features/editor/lib/sqlStatements';
+import { tokenizeSql } from '@/features/editor/lib/sqlTokens';
+import type { SchemaInfo, TableInfo } from '@/types';
+
+describe('LruCache', () => {
+ it('evicts the least recently used entry at capacity', () => {
+ const cache = new LruCache(2);
+ cache.set('a', 1);
+ cache.set('b', 2);
+ cache.get('a'); // make 'a' most recent
+ cache.set('c', 3);
+ expect(cache.get('a')).toBe(1);
+ expect(cache.get('b')).toBeUndefined();
+ expect(cache.get('c')).toBe(3);
+ });
+
+ it('overwrites an existing key without growing', () => {
+ const cache = new LruCache(2);
+ cache.set('a', 1);
+ cache.set('a', 2);
+ cache.set('b', 3);
+ expect(cache.get('a')).toBe(2);
+ expect(cache.get('b')).toBe(3);
+ });
+});
+
+describe('per-keystroke analysis is memoized', () => {
+ const schemas: SchemaInfo[] = [{ name: 'public' }];
+ const tables: TableInfo[] = [{ schema: 'public', name: 'users', type: 'table' }];
+
+ it('tokenizeSql returns the cached tokens for identical text + driver', () => {
+ expect(tokenizeSql('SELECT * FROM users', 'postgres')).toBe(tokenizeSql('SELECT * FROM users', 'postgres'));
+ expect(tokenizeSql('SELECT * FROM users', 'postgres')).not.toBe(tokenizeSql('SELECT * FROM users', 'mysql'));
+ });
+
+ it('analyzeSqlCursor returns the cached analysis for identical before-text', () => {
+ expect(analyzeSqlCursor('SELECT * FROM users WHERE ', 'postgres')).toBe(
+ analyzeSqlCursor('SELECT * FROM users WHERE ', 'postgres'),
+ );
+ });
+
+ it('parseSqlStatements returns the cached split for an identical buffer', () => {
+ expect(parseSqlStatements('SELECT 1;\nSELECT 2;', 'postgres')).toBe(
+ parseSqlStatements('SELECT 1;\nSELECT 2;', 'postgres'),
+ );
+ });
+
+ it('parseQueryContext hits only while the schema arrays keep their identity', () => {
+ const first = parseQueryContext('SELECT * FROM users', tables, schemas, 'postgres');
+ expect(parseQueryContext('SELECT * FROM users', tables, schemas, 'postgres')).toBe(first);
+ // Equal content, new array identity → miss.
+ const reloaded: TableInfo[] = [{ schema: 'public', name: 'users', type: 'table' }];
+ expect(parseQueryContext('SELECT * FROM users', reloaded, schemas, 'postgres')).not.toBe(first);
+ });
+});
diff --git a/frontend/src/features/editor/lib/sqlCache.ts b/frontend/src/features/editor/lib/sqlCache.ts
new file mode 100644
index 0000000..6320528
--- /dev/null
+++ b/frontend/src/features/editor/lib/sqlCache.ts
@@ -0,0 +1,43 @@
+// LRU for shared per-keystroke analysis (tokenize / cursor / parse).
+export class LruCache {
+ private map = new Map();
+
+ constructor(private readonly limit: number) {}
+
+ get(key: string): V | undefined {
+ const value = this.map.get(key);
+ if (value !== undefined) {
+ this.map.delete(key);
+ this.map.set(key, value);
+ }
+ return value;
+ }
+
+ set(key: string, value: V): V {
+ if (this.map.has(key)) this.map.delete(key);
+ this.map.set(key, value);
+ if (this.map.size > this.limit) {
+ const oldest = this.map.keys().next().value;
+ if (oldest !== undefined) this.map.delete(oldest);
+ }
+ return value;
+ }
+}
+
+// One LRU per dialect, keyed by the SQL text itself. Keying on the existing string
+// avoids concatenating a fresh `driver + buffer` key (and re-hashing it) on every access.
+export class DriverLruCache {
+ private byDriver = new Map>();
+
+ constructor(private readonly limit: number) {}
+
+ of(driver: string | undefined): LruCache {
+ const key = driver ?? '';
+ let cache = this.byDriver.get(key);
+ if (!cache) {
+ cache = new LruCache(this.limit);
+ this.byDriver.set(key, cache);
+ }
+ return cache;
+ }
+}
diff --git a/frontend/src/features/editor/lib/sqlCompletion.test.ts b/frontend/src/features/editor/lib/sqlCompletion.test.ts
index e8f02ef..20a5f4c 100644
--- a/frontend/src/features/editor/lib/sqlCompletion.test.ts
+++ b/frontend/src/features/editor/lib/sqlCompletion.test.ts
@@ -883,6 +883,91 @@ describe('column suggestions carry PK / NOT NULL hints', () => {
});
});
+describe('ambiguous columns across joined sources are offered qualified', () => {
+ const cols: Record = {
+ 'public.users': [
+ { name: 'id', dataType: 'int', isNullable: false, isPrimary: true, isForeign: false },
+ { name: 'email', dataType: 'text', isNullable: false, isPrimary: false, isForeign: false },
+ ],
+ 'public.orders': [
+ { name: 'id', dataType: 'int', isNullable: false, isPrimary: true, isForeign: false },
+ { name: 'total', dataType: 'numeric', isNullable: true, isPrimary: false, isForeign: false },
+ ],
+ };
+ const ambComplete = (text: string) => {
+ const ctx: CompletionContext = {
+ schemas,
+ tables,
+ columns: [],
+ tablesBySchema: { public: tables },
+ columnsByTable: cols,
+ driver: 'postgres',
+ };
+ const parsed = parseQueryContext(text, tables, schemas, 'postgres');
+ return buildCompletionItems({ ctx, text, position: text.length, parsed });
+ };
+
+ it('qualifies the shared column per source and keeps unique columns bare', () => {
+ const labels = ambComplete('SELECT * FROM users u JOIN orders o ON u.id = o.id WHERE ').map((i) => i.label);
+ expect(labels).toEqual(expect.arrayContaining(['u.id', 'o.id', 'email', 'total']));
+ expect(labels).not.toContain('id');
+ });
+
+ it('uses the table name as qualifier when there is no alias', () => {
+ const labels = ambComplete('SELECT * FROM users JOIN orders ON ').map((i) => i.label);
+ expect(labels).toEqual(expect.arrayContaining(['users.id', 'orders.id']));
+ expect(labels).not.toContain('id');
+ });
+
+ it('qualifies every column of a self-join per alias', () => {
+ const labels = ambComplete('SELECT * FROM users a JOIN users b ON ').map((i) => i.label);
+ expect(labels).toEqual(expect.arrayContaining(['a.id', 'b.id', 'a.email', 'b.email']));
+ expect(labels).not.toContain('id');
+ expect(labels).not.toContain('email');
+ });
+
+ it('keeps single-table statements fully bare', () => {
+ expect(columnLabels('SELECT * FROM users WHERE ')).toEqual(expect.arrayContaining(ALL_COLS));
+ });
+
+ it('driver-quotes the qualifier in the inserted text', () => {
+ const capTables: TableInfo[] = [
+ { schema: 'public', name: 'Users', type: 'table' },
+ { schema: 'public', name: 'orders', type: 'table' },
+ ];
+ const shared: ColumnInfo[] = [
+ { name: 'id', dataType: 'int', isNullable: false, isPrimary: true, isForeign: false },
+ ];
+ const ctx: CompletionContext = {
+ schemas,
+ tables: capTables,
+ columns: [],
+ tablesBySchema: { public: capTables },
+ columnsByTable: { 'public.Users': shared, 'public.orders': shared },
+ driver: 'postgres',
+ };
+ const text = 'SELECT * FROM "Users" JOIN orders ON ';
+ const items = buildCompletionItems({
+ ctx,
+ text,
+ position: text.length,
+ parsed: parseQueryContext(text, capTables, schemas, 'postgres'),
+ });
+ expect(items.find((i) => i.label === 'Users.id')?.insertText).toBe('"Users".id');
+ expect(items.find((i) => i.label === 'orders.id')?.insertText).toBe('orders.id');
+ });
+});
+
+describe('SELECT list offers the statement’s table refs (mid-statement edit)', () => {
+ it('offers alias, table and columns while editing the select list', () => {
+ const text = 'SELECT FROM users u';
+ const position = 'SELECT '.length;
+ const parsed = parseQueryContext(text, tables, schemas, 'postgres');
+ const labels = buildCompletionItems({ ctx: makeCtx(), text, position, parsed }).map((i) => i.label);
+ expect(labels).toEqual(expect.arrayContaining(['u', 'users', 'id', 'email', 'name']));
+ });
+});
+
describe('keywords are offered by position', () => {
it('offers statement starters only at the very start', () => {
expect(labelsOf('SEL')).toEqual(expect.arrayContaining(['SELECT']));
diff --git a/frontend/src/features/editor/lib/sqlCompletion.ts b/frontend/src/features/editor/lib/sqlCompletion.ts
index 6901aa4..bd200e1 100644
--- a/frontend/src/features/editor/lib/sqlCompletion.ts
+++ b/frontend/src/features/editor/lib/sqlCompletion.ts
@@ -4,6 +4,7 @@ import {
type SqlCursor,
type StatementShape,
} from '@/features/editor/lib/sqlContext';
+import { sqlLabels } from '@/features/editor/lib/sqlLabels';
import {
type ParsedQuery,
type QueryTableRef,
@@ -18,7 +19,7 @@ import {
keywordsForShape,
rank,
suggestColumnsForTable,
- suggestColumnsFromBindings,
+ suggestColumnsInScope,
suggestCteItems,
suggestQueryTableRefs,
suggestSchemas,
@@ -131,7 +132,7 @@ function fkJoinItems(ctx: CompletionContext, queryTables: QueryTableRef[]): Comp
items.push({
label: expr,
kind: 'field',
- detail: 'foreign key',
+ detail: sqlLabels().foreignKey,
insertText: expr,
sortText: rank(0, 0, expr),
});
@@ -141,7 +142,7 @@ function fkJoinItems(ctx: CompletionContext, queryTables: QueryTableRef[]): Comp
}
function virtualDetail(parsed: ParsedQuery, nameLc: string): string {
- return parsed.ctes.some((c) => c.toLowerCase() === nameLc) ? 'CTE column' : 'subquery column';
+ return parsed.ctes.some((c) => c.toLowerCase() === nameLc) ? sqlLabels().cteColumn : sqlLabels().subqueryColumn;
}
// Derived-table aliases are always in scope; CTEs only once referenced in FROM/JOIN.
@@ -190,14 +191,10 @@ function orderGroupItems(
ctx: CompletionContext,
slot: Extract,
queryTables: QueryTableRef[],
- bindings: Map,
): CompletionItem[] {
// Columns/tables only at the start of a sort term (after BY/comma), not after a finished one.
const items: CompletionItem[] = slot.expectsExpr
- ? [
- ...suggestQueryTableRefs(ctx, queryTables, slot.prefix),
- ...suggestColumnsFromBindings(ctx, bindings, slot.prefix),
- ]
+ ? [...suggestQueryTableRefs(ctx, queryTables, slot.prefix), ...suggestColumnsInScope(ctx, queryTables, slot.prefix)]
: [];
const keywords = slot.directionAllowed ? ['ASC', 'DESC', ...slot.trailingKeywords] : slot.trailingKeywords;
for (const kw of keywords) {
@@ -213,13 +210,13 @@ function generalItems(
shape: StatementShape,
parsed: ParsedQuery,
): CompletionItem[] {
- const { queryTables, bindings } = parsed;
+ const { queryTables } = parsed;
const items: CompletionItem[] = [];
// Identifiers only when the preceding token expects one; keywords always flow through.
if (slot.inFilter && slot.expectsExpr) {
if (shape.afterOnKeyword && slot.prefix === '') items.push(...fkJoinItems(ctx, queryTables));
items.push(...suggestQueryTableRefs(ctx, queryTables, slot.prefix));
- items.push(...suggestColumnsFromBindings(ctx, bindings, slot.prefix));
+ items.push(...suggestColumnsInScope(ctx, queryTables, slot.prefix));
items.push(...inScopeVirtualColumnItems(ctx, parsed, slot.prefix));
}
@@ -229,7 +226,9 @@ function generalItems(
}
if (slot.expectsExpr && shape.inSelectList) {
- items.push(...suggestColumnsFromBindings(ctx, bindings, slot.prefix));
+ // Include table refs so SELECT-list edits can qualify columns.
+ items.push(...suggestQueryTableRefs(ctx, queryTables, slot.prefix));
+ items.push(...suggestColumnsInScope(ctx, queryTables, slot.prefix));
}
if (slot.expectsExpr && !shape.hasFrom && !slot.inFilter) {
items.push(...suggestSchemas(ctx, slot.prefix));
@@ -240,7 +239,7 @@ function generalItems(
function completionItems(input: BuildCompletionInput, cursor: SqlCursor): CompletionItem[] {
const { ctx, parsed } = input;
const { slot, shape } = cursor;
- const { queryTables, bindings } = parsed;
+ const { queryTables } = parsed;
switch (slot.kind) {
case 'none':
@@ -253,28 +252,26 @@ function completionItems(input: BuildCompletionInput, cursor: SqlCursor): Comple
}
case 'insert-columns': {
const used = new Set(slot.used);
- return suggestColumnsFromBindings(ctx, bindings, slot.prefix).filter(
- (item) => !used.has(item.label.toLowerCase()),
- );
+ return suggestColumnsInScope(ctx, queryTables, slot.prefix).filter((item) => !used.has(item.label.toLowerCase()));
}
case 'set-column': {
- const items = suggestColumnsFromBindings(ctx, bindings, slot.prefix);
+ const items = suggestColumnsInScope(ctx, queryTables, slot.prefix);
return slot.leadingSpace ? withLeadingSpace(items) : items;
}
case 'filter-start':
return withLeadingSpace([
...(shape.afterOnKeyword ? fkJoinItems(ctx, queryTables) : []),
...suggestQueryTableRefs(ctx, queryTables, ''),
- ...suggestColumnsFromBindings(ctx, bindings, ''),
+ ...suggestColumnsInScope(ctx, queryTables, ''),
...inScopeVirtualColumnItems(ctx, parsed, ''),
]);
case 'value':
return [
- ...suggestValueItems(ctx, queryTables, bindings, slot.prefix),
+ ...suggestValueItems(ctx, queryTables, slot.prefix),
...inScopeVirtualColumnItems(ctx, parsed, slot.prefix),
];
case 'order-group': {
- const items = orderGroupItems(ctx, slot, queryTables, bindings);
+ const items = orderGroupItems(ctx, slot, queryTables);
if (slot.expectsExpr) items.push(...inScopeVirtualColumnItems(ctx, parsed, slot.prefix));
return slot.leadingSpace ? withLeadingSpace(items) : items;
}
diff --git a/frontend/src/features/editor/lib/sqlContext.ts b/frontend/src/features/editor/lib/sqlContext.ts
index 8288c85..a4f362d 100644
--- a/frontend/src/features/editor/lib/sqlContext.ts
+++ b/frontend/src/features/editor/lib/sqlContext.ts
@@ -1,3 +1,4 @@
+import { DriverLruCache } from '@/features/editor/lib/sqlCache';
import { isIdentLike, isKeyword, type SqlToken, tokenIdentText, tokenizeSql } from '@/features/editor/lib/sqlTokens';
import type { DriverType } from '@/types';
@@ -244,7 +245,15 @@ function lowerIdent(t: SqlToken): string {
return tokenIdentText(t).toLowerCase();
}
+// Same before-text is analyzed thrice per completion request.
+const cursorCache = new DriverLruCache(16);
+
export function analyzeSqlCursor(before: string, driver?: DriverType): SqlCursor {
+ const cache = cursorCache.of(driver);
+ return cache.get(before) ?? cache.set(before, analyzeCursor(before, driver));
+}
+
+function analyzeCursor(before: string, driver?: DriverType): SqlCursor {
const all = tokenizeSql(before, driver);
const len = before.length;
diff --git a/frontend/src/features/editor/lib/sqlDiagnostics.ts b/frontend/src/features/editor/lib/sqlDiagnostics.ts
index 64f2abd..5affe81 100644
--- a/frontend/src/features/editor/lib/sqlDiagnostics.ts
+++ b/frontend/src/features/editor/lib/sqlDiagnostics.ts
@@ -1,5 +1,6 @@
import { parseQueryContext } from '@/features/editor/lib/sqlQueryParse';
import { parseSqlStatements } from '@/features/editor/lib/sqlStatements';
+import { t } from '@/i18n';
import type { DriverType, SchemaInfo, TableInfo } from '@/types';
export interface SqlDiagnostic {
@@ -24,7 +25,7 @@ export function collectSchemaDiagnostics(
out.push({
start: stmt.start + ref.nameStart,
end: stmt.start + ref.nameEnd,
- message: `Unknown table "${ref.table}"`,
+ message: t('editor.sql.unknownTable', { table: ref.table }),
});
}
}
diff --git a/frontend/src/features/editor/lib/sqlHover.test.ts b/frontend/src/features/editor/lib/sqlHover.test.ts
index 030f9e0..9e9a32e 100644
--- a/frontend/src/features/editor/lib/sqlHover.test.ts
+++ b/frontend/src/features/editor/lib/sqlHover.test.ts
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest';
-import { analyzeHover, columnHoverLines } from '@/features/editor/lib/sqlHover';
+import { analyzeHover, columnHoverLines, tableColumnsMarkdown } from '@/features/editor/lib/sqlHover';
import { parseQueryContext } from '@/features/editor/lib/sqlQueryParse';
-import type { SchemaInfo, TableInfo } from '@/types';
+import type { ColumnInfo, SchemaInfo, TableInfo } from '@/types';
const schemas: SchemaInfo[] = [{ name: 'public' }];
const tables: TableInfo[] = [
@@ -70,4 +70,30 @@ describe('analyzeHover', () => {
);
expect(lines).toEqual(['**email** · text · not null', 'column of public.users']);
});
+
+ it('marks table and alias hovers for a column-list append', () => {
+ expect(hoverAt('SELECT * FROM users', 'users')?.tableColumns).toEqual({ schema: 'public', table: 'users' });
+ const alias = hoverAt('SELECT * FROM users u WHERE u.id = 1', 'u ', 1);
+ expect(alias?.tableColumns).toEqual({ schema: 'public', table: 'users' });
+ });
+
+ it('renders the column list as a markdown table, capped', () => {
+ const cols: ColumnInfo[] = [
+ { name: 'id', dataType: 'int', isNullable: false, isPrimary: true, isForeign: false },
+ { name: 'email', dataType: 'text', isNullable: false, isPrimary: false, isForeign: false },
+ ];
+ expect(tableColumnsMarkdown(cols)).toEqual([
+ '| column | type |\n| --- | --- |\n| id | int · PK |\n| email | text · not null |',
+ ]);
+ expect(tableColumnsMarkdown([])).toEqual([]);
+
+ const many: ColumnInfo[] = Array.from({ length: 35 }, (_, i) => ({
+ name: `c${i}`,
+ dataType: 'int',
+ isNullable: true,
+ isPrimary: false,
+ isForeign: false,
+ }));
+ expect(tableColumnsMarkdown(many)[1]).toBe('… 5 more columns');
+ });
});
diff --git a/frontend/src/features/editor/lib/sqlHover.ts b/frontend/src/features/editor/lib/sqlHover.ts
index e02ec8c..fad5222 100644
--- a/frontend/src/features/editor/lib/sqlHover.ts
+++ b/frontend/src/features/editor/lib/sqlHover.ts
@@ -1,7 +1,9 @@
+import { sqlLabels } from '@/features/editor/lib/sqlLabels';
import { type ParsedQuery, resolveDotCompletion, type TableBinding } from '@/features/editor/lib/sqlQueryParse';
import { QUOTE_FORCING_KEYWORDS, unquoteIdent } from '@/features/editor/lib/sqlQuoting';
-import { columnDetail } from '@/features/editor/lib/sqlSuggestions';
+import { columnDetail, relationTypeLabel } from '@/features/editor/lib/sqlSuggestions';
import { isIdentLike, type SqlToken, tokenIdentText, tokenizeSql } from '@/features/editor/lib/sqlTokens';
+import { t } from '@/i18n';
import type { ColumnInfo, DriverType, SchemaInfo, TableInfo } from '@/types';
// Immediate answer (`lines`) or a column lookup the caller resolves via cached loads.
@@ -10,19 +12,37 @@ export interface HoverQuery {
end: number;
lines?: string[];
columnLookup?: { bindings: TableBinding[]; name: string };
+ tableColumns?: TableBinding; // Lazy column list for table/alias hover.
}
export function columnHoverLines(col: ColumnInfo, table: TableBinding): string[] {
const where = table.schema ? `${table.schema}.${table.table}` : table.table;
- return [`**${col.name}** · ${columnDetail(col)}`, `column of ${where}`];
+ return [`**${col.name}** · ${columnDetail(col)}`, t('editor.sql.columnOf', { target: where })];
}
-function tableLines(t: TableInfo): string[] {
- return [`**${t.name}** · ${t.type || 'table'}`, t.schema ? `schema ${t.schema}` : ''].filter(Boolean);
+export function tableColumnsMarkdown(cols: ColumnInfo[], max = 30): string[] {
+ if (cols.length === 0) return [];
+ const shown = cols.slice(0, max);
+ const rows = shown.map((c) => `| ${c.name} | ${columnDetail(c)} |`);
+ const table = [`| ${sqlLabels().column} | ${sqlLabels().type} |`, '| --- | --- |', ...rows].join('\n');
+ return cols.length > shown.length
+ ? [table, t('editor.sql.moreColumns', { count: cols.length - shown.length })]
+ : [table];
+}
+
+function tableLines(info: TableInfo): string[] {
+ return [
+ t('editor.sql.nameKind', { name: `**${info.name}**`, kind: relationTypeLabel(info.type) }),
+ info.schema ? t('editor.sql.schemaName', { name: info.schema }) : '',
+ ].filter(Boolean);
+}
+
+function virtualKindLabel(isCte: boolean): string {
+ return isCte ? sqlLabels().cte : sqlLabels().subquery;
}
function tokenAt(tokens: SqlToken[], offset: number): SqlToken | undefined {
- return tokens.find((t) => offset >= t.start && offset < t.end && isIdentLike(t));
+ return tokens.find((tok) => offset >= tok.start && offset < tok.end && isIdentLike(tok));
}
export function analyzeHover(
@@ -33,7 +53,7 @@ export function analyzeHover(
schemas: SchemaInfo[],
driver: DriverType,
): HoverQuery | null {
- const tokens = tokenizeSql(stmtText, driver).filter((t) => t.kind !== 'comment');
+ const tokens = tokenizeSql(stmtText, driver).filter((tok) => tok.kind !== 'comment');
const tok = tokenAt(tokens, offset);
if (!tok) return null;
// Bare keywords aren't identifiers; quoted tokens always are.
@@ -62,8 +82,8 @@ export function analyzeHover(
const virtual = parsed.virtualColumns.get(qualLc);
if (virtual) {
if (!virtual.some((c) => c.toLowerCase() === nameLc)) return null;
- const kind = parsed.ctes.some((c) => c.toLowerCase() === qualLc) ? 'CTE' : 'subquery';
- return { ...span, lines: [`**${name}**`, `column of ${kind} ${qualLc}`] };
+ const kind = virtualKindLabel(parsed.ctes.some((c) => c.toLowerCase() === qualLc));
+ return { ...span, lines: [`**${name}**`, t('editor.sql.columnOf', { target: `${kind} ${qualLc}` })] };
}
const binding = resolveDotCompletion({ segments }, parsed.bindings, tables, schemas, driver);
if (binding) return { ...span, columnLookup: { bindings: [binding], name: nameLc } };
@@ -73,28 +93,51 @@ export function analyzeHover(
// Alias or table referenced by this query.
const bound = parsed.bindings.get(nameLc);
if (bound) {
- const info = tables.find((t) => t.name === bound.table && t.schema === bound.schema);
+ const info = tables.find((tbl) => tbl.name === bound.table && tbl.schema === bound.schema);
if (bound.table.toLowerCase() !== nameLc) {
- return { ...span, lines: [`**${name}** · alias for ${bound.table}`, ...(info ? tableLines(info).slice(1) : [])] };
+ return {
+ ...span,
+ lines: [
+ t('editor.sql.nameKind', {
+ name: `**${name}**`,
+ kind: t('editor.sql.aliasFor', { table: bound.table }),
+ }),
+ ...(info ? tableLines(info).slice(1) : []),
+ ],
+ tableColumns: info ? bound : undefined,
+ };
}
- if (info) return { ...span, lines: tableLines(info) };
+ if (info) return { ...span, lines: tableLines(info), tableColumns: bound };
}
// CTE / derived-table alias.
const virtual = parsed.virtualColumns.get(nameLc);
if (virtual) {
- const kind = parsed.ctes.some((c) => c.toLowerCase() === nameLc) ? 'CTE' : 'subquery';
+ const kind = virtualKindLabel(parsed.ctes.some((c) => c.toLowerCase() === nameLc));
const cols = virtual.length > 0 ? virtual.slice(0, 8).join(', ') + (virtual.length > 8 ? ', …' : '') : '';
- return { ...span, lines: [`**${name}** · ${kind}`, cols && `columns: ${cols}`].filter(Boolean) as string[] };
+ return {
+ ...span,
+ lines: [
+ t('editor.sql.nameKind', { name: `**${name}**`, kind }),
+ cols ? t('editor.sql.columnsList', { cols }) : '',
+ ].filter(Boolean) as string[],
+ };
}
// Any table in the connected schema.
- const table = tables.find((t) => t.name.toLowerCase() === nameLc);
- if (table) return { ...span, lines: tableLines(table) };
+ const table = tables.find((tbl) => tbl.name.toLowerCase() === nameLc);
+ if (table) {
+ return { ...span, lines: tableLines(table), tableColumns: { schema: table.schema, table: table.name } };
+ }
// A schema name.
const schema = schemas.find((s) => s.name.toLowerCase() === nameLc);
- if (schema) return { ...span, lines: [`**${schema.name}** · schema`] };
+ if (schema) {
+ return {
+ ...span,
+ lines: [t('editor.sql.nameKind', { name: `**${schema.name}**`, kind: sqlLabels().schema })],
+ };
+ }
// Bare column: search the query's in-scope tables (loaded lazily by the caller).
const candidates = [...parsed.bindings.values()].filter(
diff --git a/frontend/src/features/editor/lib/sqlLabels.test.ts b/frontend/src/features/editor/lib/sqlLabels.test.ts
new file mode 100644
index 0000000..71cf02d
--- /dev/null
+++ b/frontend/src/features/editor/lib/sqlLabels.test.ts
@@ -0,0 +1,20 @@
+import i18n from 'i18next';
+import { describe, expect, it } from 'vitest';
+import { sqlLabels } from '@/features/editor/lib/sqlLabels';
+
+describe('sqlLabels', () => {
+ it('returns the same memoized object until the language changes', async () => {
+ await i18n.changeLanguage('en');
+ const first = sqlLabels();
+ expect(first.notNull).toBe('not null');
+ expect(sqlLabels()).toBe(first);
+
+ await i18n.changeLanguage('de');
+ const german = sqlLabels();
+ expect(german).not.toBe(first);
+ expect(german.table).toBe('Tabelle');
+
+ await i18n.changeLanguage('en');
+ expect(sqlLabels().table).toBe('table');
+ });
+});
diff --git a/frontend/src/features/editor/lib/sqlLabels.ts b/frontend/src/features/editor/lib/sqlLabels.ts
new file mode 100644
index 0000000..59e04fe
--- /dev/null
+++ b/frontend/src/features/editor/lib/sqlLabels.ts
@@ -0,0 +1,44 @@
+import { subscribeLanguageChanged, t } from '@/i18n';
+
+// Static suggestion/hover copy, resolved once per language instead of per item
+// (completion emits hundreds of items per keystroke).
+export interface SqlLabels {
+ pk: string;
+ fk: string;
+ notNull: string;
+ table: string;
+ view: string;
+ schema: string;
+ cte: string;
+ subquery: string;
+ column: string;
+ type: string;
+ foreignKey: string;
+ cteColumn: string;
+ subqueryColumn: string;
+}
+
+let cached: SqlLabels | null = null;
+
+subscribeLanguageChanged(() => {
+ cached = null;
+});
+
+export function sqlLabels(): SqlLabels {
+ cached ??= {
+ pk: t('editor.sql.pk'),
+ fk: t('editor.sql.fk'),
+ notNull: t('editor.sql.notNull'),
+ table: t('editor.sql.table'),
+ view: t('editor.sql.view'),
+ schema: t('editor.sql.schema'),
+ cte: t('editor.sql.cte'),
+ subquery: t('editor.sql.subquery'),
+ column: t('editor.sql.column'),
+ type: t('editor.sql.type'),
+ foreignKey: t('editor.sql.foreignKey'),
+ cteColumn: t('editor.sql.cteColumn'),
+ subqueryColumn: t('editor.sql.subqueryColumn'),
+ };
+ return cached;
+}
diff --git a/frontend/src/features/editor/lib/sqlQueryParse.ts b/frontend/src/features/editor/lib/sqlQueryParse.ts
index 72316e2..8b58f7c 100644
--- a/frontend/src/features/editor/lib/sqlQueryParse.ts
+++ b/frontend/src/features/editor/lib/sqlQueryParse.ts
@@ -1,3 +1,4 @@
+import { DriverLruCache } from '@/features/editor/lib/sqlCache';
import { ALIAS_STOP_WORDS, unquoteIdent } from '@/features/editor/lib/sqlQuoting';
import {
isIdentLike,
@@ -238,12 +239,31 @@ function collectCtes(tokens: SqlToken[], virtualColumns: Map):
return ctes;
}
+// Schema array identity is part of the hit check (resolution depends on it).
+interface ParseCacheEntry {
+ tables: TableInfo[];
+ schemas: SchemaInfo[];
+ result: ParsedQuery;
+}
+
+// Sized so a typical multi-statement diagnostics pass stays warm.
+const parseCache = new DriverLruCache(256);
+
export function parseQueryContext(
sql: string,
tables: TableInfo[],
schemas: SchemaInfo[],
driver: DriverType,
): ParsedQuery {
+ const cache = parseCache.of(driver);
+ const hit = cache.get(sql);
+ if (hit && hit.tables === tables && hit.schemas === schemas) return hit.result;
+ const result = parseQuery(sql, tables, schemas, driver);
+ cache.set(sql, { tables, schemas, result });
+ return result;
+}
+
+function parseQuery(sql: string, tables: TableInfo[], schemas: SchemaInfo[], driver: DriverType): ParsedQuery {
const tokens = tokenizeSql(sql, driver);
const queryTables: QueryTableRef[] = [];
const virtualColumns = new Map();
diff --git a/frontend/src/features/editor/lib/sqlStatements.ts b/frontend/src/features/editor/lib/sqlStatements.ts
index 20a98f7..5c7af3a 100644
--- a/frontend/src/features/editor/lib/sqlStatements.ts
+++ b/frontend/src/features/editor/lib/sqlStatements.ts
@@ -1,3 +1,4 @@
+import { DriverLruCache } from '@/features/editor/lib/sqlCache';
import {
findBlockCommentEnd,
findLineCommentEnd,
@@ -47,7 +48,15 @@ function firstCodeOffset(text: string, from: number, to: number, opts: SqlLexOpt
// mysql-client `DELIMITER xx` line: switches the terminator so procedure bodies can contain `;`.
const DELIMITER_LINE_RE = /DELIMITER[ \t]+(\S+)[ \t]*(?:\r?\n|$)/iy;
+// Whole-buffer splits shared by glyphs / completion / diagnostics.
+const statementCache = new DriverLruCache(8);
+
export function parseSqlStatements(sql: string, driver?: DriverType): SqlStatement[] {
+ const cache = statementCache.of(driver);
+ return cache.get(sql) ?? cache.set(sql, splitStatements(sql, driver));
+}
+
+function splitStatements(sql: string, driver?: DriverType): SqlStatement[] {
const opts = lexOptionsFor(driver);
const clientDelimiters = driver === 'mysql';
const statements: SqlStatement[] = [];
diff --git a/frontend/src/features/editor/lib/sqlSuggestions.ts b/frontend/src/features/editor/lib/sqlSuggestions.ts
index 77dbd8c..1caf01b 100644
--- a/frontend/src/features/editor/lib/sqlSuggestions.ts
+++ b/frontend/src/features/editor/lib/sqlSuggestions.ts
@@ -1,6 +1,8 @@
import type { StatementShape } from '@/features/editor/lib/sqlContext';
+import { sqlLabels } from '@/features/editor/lib/sqlLabels';
import type { QueryTableRef, TableBinding } from '@/features/editor/lib/sqlQueryParse';
import { columnCacheKey, formatSqlIdentifier } from '@/features/editor/lib/sqlQuoting';
+import { t } from '@/i18n';
import type { ColumnInfo, DriverType, SchemaInfo, TableInfo } from '@/types';
export interface CompletionContext {
@@ -143,14 +145,25 @@ export function rank(tier: 0 | 1 | 2 | 3 | 4 | 5, score: number, label: string):
// Column hint shown in the suggestion's detail line: type plus PK / FK / NOT NULL markers.
export function columnDetail(c: ColumnInfo): string {
- const tags: string[] = [];
- if (c.isPrimary) tags.push('PK');
- if (c.isForeign) tags.push('FK');
- if (tags.length > 0) return `${c.dataType} · ${tags.join(' · ')}`;
- if (!c.isNullable) return `${c.dataType} · not null`;
+ const labels = sqlLabels();
+ if (c.isPrimary || c.isForeign) {
+ const tags = c.isPrimary && c.isForeign ? `${labels.pk} · ${labels.fk}` : c.isPrimary ? labels.pk : labels.fk;
+ return `${c.dataType} · ${tags}`;
+ }
+ if (!c.isNullable) return `${c.dataType} · ${labels.notNull}`;
return c.dataType;
}
+// Known relation kinds; unknown catalog values pass through.
+export function relationTypeLabel(type?: string): string {
+ if (!type || type === 'table') return sqlLabels().table;
+ const raw = type.trim();
+ const key = raw.toLowerCase();
+ if (key === 'table' || key === 'base table') return sqlLabels().table;
+ if (key === 'view') return sqlLabels().view;
+ return raw;
+}
+
export function keywordItem(kw: string, lcPrefix: string): CompletionItem | null {
const score = matchScore(kw, lcPrefix);
if (score < 0) return null;
@@ -220,7 +233,7 @@ export function suggestCteItems(ctes: string[], lcPrefix: string, driver: Driver
items.push({
label: name,
kind: 'class',
- detail: 'CTE',
+ detail: sqlLabels().cte,
insertText: formatSqlIdentifier(name, driver),
filterText: formatSqlIdentifier(name, driver),
// Tier 0 (query-local): ranks above the table list so it survives the 100-item cap.
@@ -241,10 +254,11 @@ export function suggestTables(ctx: CompletionContext, lcPrefix: string, schemaFi
const score = matchScore(t.name, lcPrefix);
if (score < 0) continue;
const insert = formatSqlIdentifier(t.name, ctx.driver);
+ const kind = relationTypeLabel(schemaFilter ? 'table' : t.type || 'table');
items.push({
label: t.name,
kind: 'class',
- detail: schemaFilter ? 'table' : t.type || 'table',
+ detail: kind,
insertText: insert,
filterText: insert,
sortText: rank(1, score, t.name),
@@ -261,7 +275,7 @@ export function suggestSchemas(ctx: CompletionContext, lcPrefix: string): Comple
items.push({
label: s.name,
kind: 'module',
- detail: 'schema',
+ detail: sqlLabels().schema,
insertText: formatSqlIdentifier(s.name, ctx.driver),
sortText: rank(3, score, s.name),
});
@@ -288,7 +302,9 @@ export function suggestQueryTableRefs(
items.push({
label: ref.table,
kind: 'class',
- detail: ref.schema ? `${ref.schema} · table` : 'table',
+ detail: ref.schema
+ ? t('editor.sql.schemaTable', { schema: ref.schema, type: sqlLabels().table })
+ : sqlLabels().table,
insertText: insert,
filterText: insert,
sortText: rank(0, score, ref.table),
@@ -306,7 +322,7 @@ export function suggestQueryTableRefs(
items.push({
label: ref.alias,
kind: 'class',
- detail: `alias → ${ref.table}`,
+ detail: t('editor.sql.aliasArrow', { table: ref.table }),
insertText: insert,
filterText: insert,
sortText: rank(0, score, ref.alias),
@@ -319,22 +335,73 @@ export function suggestQueryTableRefs(
return items;
}
-export function suggestColumnsFromBindings(
+function countColumnNames(sources: { cols: ColumnInfo[] }[]): Map {
+ const counts = new Map();
+ const perSource = new Set();
+ for (const s of sources) {
+ perSource.clear();
+ for (const c of s.cols) {
+ const key = c.name.toLowerCase();
+ if (perSource.has(key)) continue;
+ perSource.add(key);
+ counts.set(key, (counts.get(key) ?? 0) + 1);
+ }
+ }
+ return counts;
+}
+
+// In-scope columns; names shared by multiple sources are offered as `alias.col`.
+export function suggestColumnsInScope(
ctx: CompletionContext,
- bindings: Map,
+ queryTables: QueryTableRef[],
lcPrefix: string,
+ seen?: Set,
): CompletionItem[] {
- const items: CompletionItem[] = [];
- const seen = new Set();
- const seenTables = new Set();
-
- for (const binding of bindings.values()) {
- const tk = columnCacheKey(binding.schema, binding.table);
- if (seenTables.has(tk)) continue;
- seenTables.add(tk);
- pushColumnItems(items, ctx.columnsByTable[tk] || [], 0, lcPrefix, ctx.driver, seen);
+ const sources: { ref: QueryTableRef; cols: ColumnInfo[] }[] = [];
+ const seenRefs = new Set();
+ for (const ref of queryTables) {
+ const refKey = `${columnCacheKey(ref.schema, ref.table)}|${(ref.alias ?? '').toLowerCase()}`;
+ if (seenRefs.has(refKey)) continue;
+ seenRefs.add(refKey);
+ sources.push({ ref, cols: ctx.columnsByTable[columnCacheKey(ref.schema, ref.table)] || [] });
}
+ // Count per source (self-joins: one per alias). One source cannot be ambiguous with itself.
+ const nameCount = sources.length > 1 ? countColumnNames(sources) : null;
+
+ const items: CompletionItem[] = [];
+ for (const s of sources) {
+ const emitted = new Set();
+ for (const c of s.cols) {
+ const key = c.name.toLowerCase();
+ if (emitted.has(key)) continue;
+ emitted.add(key);
+ const score = matchScore(c.name, lcPrefix);
+ if (score < 0) continue;
+ if (nameCount !== null && (nameCount.get(key) ?? 0) > 1) {
+ const qualifier = s.ref.alias ?? s.ref.table;
+ const label = `${qualifier}.${c.name}`;
+ seen?.add(key);
+ items.push({
+ label,
+ kind: 'field',
+ detail: columnDetail(c),
+ insertText: `${formatSqlIdentifier(qualifier, ctx.driver)}.${formatSqlIdentifier(c.name, ctx.driver)}`,
+ sortText: rank(0, score, label),
+ });
+ } else {
+ if (seen?.has(key)) continue;
+ seen?.add(key);
+ items.push({
+ label: c.name,
+ kind: 'field',
+ detail: columnDetail(c),
+ insertText: formatSqlIdentifier(c.name, ctx.driver),
+ sortText: rank(0, score, c.name),
+ });
+ }
+ }
+ }
return items;
}
@@ -343,7 +410,6 @@ const VALUE_LITERALS = ['NULL', 'TRUE', 'FALSE', 'DEFAULT'];
export function suggestValueItems(
ctx: CompletionContext,
queryTables: QueryTableRef[],
- bindings: Map,
lcPrefix: string,
): CompletionItem[] {
const items: CompletionItem[] = [];
@@ -360,16 +426,7 @@ export function suggestValueItems(
if (item) items.push(item);
}
- for (const binding of bindings.values()) {
- pushColumnItems(
- items,
- ctx.columnsByTable[columnCacheKey(binding.schema, binding.table)] || [],
- 0,
- lcPrefix,
- ctx.driver,
- seenCols,
- );
- }
+ items.push(...suggestColumnsInScope(ctx, queryTables, lcPrefix, seenCols));
pushColumnItems(items, ctx.columns, 1, lcPrefix, ctx.driver, seenCols);
return items;
diff --git a/frontend/src/features/editor/lib/sqlTokens.ts b/frontend/src/features/editor/lib/sqlTokens.ts
index 724ecc9..2b25f72 100644
--- a/frontend/src/features/editor/lib/sqlTokens.ts
+++ b/frontend/src/features/editor/lib/sqlTokens.ts
@@ -1,3 +1,4 @@
+import { DriverLruCache } from '@/features/editor/lib/sqlCache';
import {
findBlockCommentEnd,
findLineCommentEnd,
@@ -34,8 +35,17 @@ const WS_RE = /\s/;
const TWO_CHAR_OPS = new Set(['<=', '>=', '<>', '!=', '::', '||', ':=']);
const PUNCT = new Set(['.', ',', '(', ')', ';']);
-// One pass; $tag$ delimiters are ops so dollar-quoted bodies stay tokenized (completion works inside them).
+// Shared across providers per keystroke; treat returned tokens as immutable.
+// Capacity covers a typical multi-statement diagnostics pass.
+const tokenCache = new DriverLruCache(256);
+
export function tokenizeSql(text: string, driver?: DriverType): SqlToken[] {
+ const cache = tokenCache.of(driver);
+ return cache.get(text) ?? cache.set(text, tokenize(text, driver));
+}
+
+// One pass; $tag$ delimiters are ops so dollar-quoted bodies stay tokenized (completion works inside them).
+function tokenize(text: string, driver?: DriverType): SqlToken[] {
const opts = lexOptionsFor(driver);
const len = text.length;
const tokens: SqlToken[] = [];
diff --git a/frontend/src/i18n/locales/bg.json b/frontend/src/i18n/locales/bg.json
index 67d653b..bd8f030 100644
--- a/frontend/src/i18n/locales/bg.json
+++ b/frontend/src/i18n/locales/bg.json
@@ -196,7 +196,31 @@
"actionRunSelection": "Изпълни избраното",
"actionRunAll": "Изпълни всичко",
"actionSaveQuery": "Запази заявка",
- "actionRenameSaved": "Преименувай запазена заявка"
+ "actionRenameSaved": "Преименувай запазена заявка",
+ "sql": {
+ "pk": "PK",
+ "fk": "FK",
+ "notNull": "не е null",
+ "table": "таблица",
+ "view": "изглед",
+ "schema": "схема",
+ "column": "колона",
+ "type": "тип",
+ "cte": "CTE",
+ "subquery": "подзаявка",
+ "cteColumn": "колона от CTE",
+ "subqueryColumn": "колона от подзаявка",
+ "foreignKey": "външен ключ",
+ "aliasArrow": "псевдоним → {{table}}",
+ "schemaTable": "{{schema}} · {{type}}",
+ "unknownTable": "Неизвестна таблица \"{{table}}\"",
+ "columnOf": "колона от {{target}}",
+ "schemaName": "схема {{name}}",
+ "nameKind": "{{name}} · {{kind}}",
+ "aliasFor": "псевдоним за {{table}}",
+ "columnsList": "колони: {{cols}}",
+ "moreColumns": "… още {{count}} колони"
+ }
},
"results": {
"noResults": "Няма резултати",
diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json
index 5c404cb..1d6265b 100644
--- a/frontend/src/i18n/locales/de.json
+++ b/frontend/src/i18n/locales/de.json
@@ -196,7 +196,31 @@
"actionRunSelection": "Auswahl ausführen",
"actionRunAll": "Alles ausführen",
"actionSaveQuery": "Abfrage speichern",
- "actionRenameSaved": "Gespeicherte Abfrage umbenennen"
+ "actionRenameSaved": "Gespeicherte Abfrage umbenennen",
+ "sql": {
+ "pk": "PK",
+ "fk": "FK",
+ "notNull": "nicht null",
+ "table": "Tabelle",
+ "view": "Sicht",
+ "schema": "Schema",
+ "column": "Spalte",
+ "type": "Typ",
+ "cte": "CTE",
+ "subquery": "Unterabfrage",
+ "cteColumn": "CTE-Spalte",
+ "subqueryColumn": "Unterabfrage-Spalte",
+ "foreignKey": "Fremdschlüssel",
+ "aliasArrow": "Alias → {{table}}",
+ "schemaTable": "{{schema}} · {{type}}",
+ "unknownTable": "Unbekannte Tabelle \"{{table}}\"",
+ "columnOf": "Spalte von {{target}}",
+ "schemaName": "Schema {{name}}",
+ "nameKind": "{{name}} · {{kind}}",
+ "aliasFor": "Alias für {{table}}",
+ "columnsList": "Spalten: {{cols}}",
+ "moreColumns": "… {{count}} weitere Spalten"
+ }
},
"results": {
"noResults": "Keine Ergebnisse",
diff --git a/frontend/src/i18n/locales/en.json b/frontend/src/i18n/locales/en.json
index d0eb49c..f501afd 100644
--- a/frontend/src/i18n/locales/en.json
+++ b/frontend/src/i18n/locales/en.json
@@ -196,7 +196,31 @@
"actionRunSelection": "Run Selection",
"actionRunAll": "Run All",
"actionSaveQuery": "Save Query",
- "actionRenameSaved": "Rename Saved Query"
+ "actionRenameSaved": "Rename Saved Query",
+ "sql": {
+ "pk": "PK",
+ "fk": "FK",
+ "notNull": "not null",
+ "table": "table",
+ "view": "view",
+ "schema": "schema",
+ "column": "column",
+ "type": "type",
+ "cte": "CTE",
+ "subquery": "subquery",
+ "cteColumn": "CTE column",
+ "subqueryColumn": "subquery column",
+ "foreignKey": "foreign key",
+ "aliasArrow": "alias → {{table}}",
+ "schemaTable": "{{schema}} · {{type}}",
+ "unknownTable": "Unknown table \"{{table}}\"",
+ "columnOf": "column of {{target}}",
+ "schemaName": "schema {{name}}",
+ "nameKind": "{{name}} · {{kind}}",
+ "aliasFor": "alias for {{table}}",
+ "columnsList": "columns: {{cols}}",
+ "moreColumns": "… {{count}} more columns"
+ }
},
"results": {
"noResults": "No results",
diff --git a/frontend/src/shared/lib/appInfo.ts b/frontend/src/shared/lib/appInfo.ts
index 81eb880..76a586b 100644
--- a/frontend/src/shared/lib/appInfo.ts
+++ b/frontend/src/shared/lib/appInfo.ts
@@ -11,7 +11,7 @@ export interface AppInfo {
/** Fallback when Go binding is unavailable (dev in browser). */
export const DEFAULT_APP_INFO: AppInfo = {
name: 'XenSQL',
- version: '1.4.2',
+ version: '1.4.3',
author: 'Bare7a',
email: 'bare7a@gmail.com',
website: 'https://xensql.bare7a.eu',
diff --git a/frontend/src/test/setupI18n.ts b/frontend/src/test/setupI18n.ts
new file mode 100644
index 0000000..a099c23
--- /dev/null
+++ b/frontend/src/test/setupI18n.ts
@@ -0,0 +1,19 @@
+import i18n from 'i18next';
+import bg from '@/i18n/locales/bg.json';
+import de from '@/i18n/locales/de.json';
+import en from '@/i18n/locales/en.json';
+
+// Node has no DOM; init i18n so SQL helpers that call t() resolve English copy.
+if (!i18n.isInitialized) {
+ await i18n.init({
+ resources: {
+ en: { translation: en },
+ de: { translation: de },
+ bg: { translation: bg },
+ },
+ lng: 'en',
+ fallbackLng: 'en',
+ interpolation: { escapeValue: false },
+ returnEmptyString: false,
+ });
+}
diff --git a/frontend/src/vite-env.d.ts b/frontend/src/vite-env.d.ts
index b85b3d4..11f02fe 100644
--- a/frontend/src/vite-env.d.ts
+++ b/frontend/src/vite-env.d.ts
@@ -1,12 +1 @@
///
-
-// Monaco ships no .d.ts for its deep ESM subpaths (we import them directly to keep
-// unused language services/workers out of the bundle - see features/editor/lib/monacoSetup.ts).
-// Vite resolves the real files at build time; these just satisfy tsc.
-declare module 'monaco-editor/esm/vs/editor/editor.api' {
- export * from 'monaco-editor';
-}
-declare module 'monaco-editor/esm/vs/language/json/monaco.contribution';
-declare module 'monaco-editor/esm/vs/basic-languages/sql/sql.contribution';
-declare module 'monaco-editor/esm/vs/basic-languages/xml/xml.contribution';
-declare module 'monaco-editor/esm/vs/basic-languages/html/html.contribution';
diff --git a/frontend/vitest.bench.config.ts b/frontend/vitest.bench.config.ts
new file mode 100644
index 0000000..0ee5f7e
--- /dev/null
+++ b/frontend/vitest.bench.config.ts
@@ -0,0 +1,25 @@
+import path from 'node:path';
+import { fileURLToPath } from 'node:url';
+import { defineConfig } from 'vitest/config';
+
+const __dirname = path.dirname(fileURLToPath(import.meta.url));
+
+export default defineConfig({
+ resolve: {
+ alias: {
+ '@': path.resolve(__dirname, 'src'),
+ '@bindings': path.resolve(__dirname, 'bindings'),
+ '@wailsio/runtime': path.resolve(__dirname, 'src/test/wailsioRuntimeStub.ts'),
+ },
+ },
+ test: {
+ environment: 'node',
+ include: ['bench/**/*.bench.test.ts'],
+ pool: 'forks',
+ maxWorkers: 1,
+ execArgv: ['--expose-gc'],
+ testTimeout: 900_000,
+ hookTimeout: 900_000,
+ globals: false,
+ },
+});
diff --git a/frontend/vitest.config.ts b/frontend/vitest.config.ts
index a192e50..fd9b133 100644
--- a/frontend/vitest.config.ts
+++ b/frontend/vitest.config.ts
@@ -22,6 +22,7 @@ export default defineConfig({
// Tests can live anywhere under src - co-located with the modules they
// cover. Feature folders are about to hold their own *.test.ts files.
include: ['src/**/*.test.ts'],
+ setupFiles: ['src/test/setupI18n.ts'],
globals: false,
},
});
diff --git a/go.mod b/go.mod
index 7e2275d..0483a59 100644
--- a/go.mod
+++ b/go.mod
@@ -6,8 +6,8 @@ require (
github.com/go-sql-driver/mysql v1.10.0
github.com/google/uuid v1.6.0
github.com/jackc/pgx/v5 v5.10.0
- github.com/wailsapp/wails/v3 v3.0.0-alpha2.115
- modernc.org/sqlite v1.53.0
+ github.com/wailsapp/wails/v3 v3.0.0-alpha2.117
+ modernc.org/sqlite v1.54.0
)
require (
@@ -22,14 +22,14 @@ require (
github.com/jackc/puddle/v2 v2.2.2 // indirect
github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect
github.com/mattn/go-colorable v0.1.15 // indirect
- github.com/mattn/go-isatty v0.0.22 // indirect
+ github.com/mattn/go-isatty v0.0.23 // indirect
github.com/ncruces/go-strftime v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
- golang.org/x/mod v0.37.0 // indirect
- golang.org/x/sync v0.21.0 // indirect
- golang.org/x/sys v0.46.0 // indirect
- golang.org/x/text v0.38.0 // indirect
- modernc.org/libc v1.73.5 // indirect
+ golang.org/x/mod v0.38.0 // indirect
+ golang.org/x/sync v0.22.0 // indirect
+ golang.org/x/sys v0.47.0 // indirect
+ golang.org/x/text v0.40.0 // indirect
+ modernc.org/libc v1.74.3 // indirect
modernc.org/mathutil v1.7.1 // indirect
modernc.org/memory v1.11.0 // indirect
)
diff --git a/go.sum b/go.sum
index 3b8352c..53174e5 100644
--- a/go.sum
+++ b/go.sum
@@ -39,8 +39,8 @@ github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
github.com/mattn/go-colorable v0.1.15 h1:+u9SLTRGnXv73cEsnsmoZBom+dMU88B2M0aDcWy0/jY=
github.com/mattn/go-colorable v0.1.15/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
-github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
-github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
+github.com/mattn/go-isatty v0.0.23 h1:cYwCQTQf3HB6xUC+BtyCLZNr7IzbOmoZbmssVNzSyiQ=
+github.com/mattn/go-isatty v0.0.23/go.mod h1:nMCL3Zebbrt45jsMDgnfIwz6ydEQApk5oEI3HqDio6A=
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@@ -52,28 +52,28 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
-github.com/wailsapp/wails/v3 v3.0.0-alpha2.115 h1:AwVincOURTpVeSNWAdZ9/xXnfrsHCBtt6ry9I5L8SZM=
-github.com/wailsapp/wails/v3 v3.0.0-alpha2.115/go.mod h1:74WH2FScMsgucZvHHvv7eOefDXCm/CjuIxqhhZgPhKg=
-golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
-golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
-golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
-golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+github.com/wailsapp/wails/v3 v3.0.0-alpha2.117 h1:udyjqPG3AIgkod5QDR/WblCkpV8R86BFPSrsWxSyt5Y=
+github.com/wailsapp/wails/v3 v3.0.0-alpha2.117/go.mod h1:74WH2FScMsgucZvHHvv7eOefDXCm/CjuIxqhhZgPhKg=
+golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk=
+golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40=
+golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
+golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
-golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
-golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
-golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
-golang.org/x/tools v0.46.0 h1:7jTurBkPZu4moS/Uy4OQT1M+QBlsj3wejyZwsT8Z7rk=
-golang.org/x/tools v0.46.0/go.mod h1:FrD85F8l+NWL+9XWBSyVSHO6Ne4jutsfIFba7AWQ5Ys=
+golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
+golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs=
+golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY=
+golang.org/x/tools v0.47.0 h1:7Kn5x/d1svx/PzryTsqeoZN4TZwqeH5pGWjefhLi/1Q=
+golang.org/x/tools v0.47.0/go.mod h1:dFHnyTvFWY212G+h7ZY4Vsp/K3U4/7W9TyVaAul8uCA=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-modernc.org/cc/v4 v4.29.0 h1:CXgwL8cvxmyzBQZzbSl/6xFtMCryb6u8IOqDci39cgc=
-modernc.org/cc/v4 v4.29.0/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
-modernc.org/ccgo/v4 v4.34.5 h1:hcwnthv2/LBl+mRLOYwnQA/LuW44Oln1NQlWppNaS1Q=
-modernc.org/ccgo/v4 v4.34.5/go.mod h1:aow0HNkO30OSA/2NrtDXkis92ff8ZFiDOmDOPhqhF8U=
+modernc.org/cc/v4 v4.29.1 h1:MKgdCV3WykTSPqpVrnxdEDS0HEd2FHpKZDzxzU5LyeI=
+modernc.org/cc/v4 v4.29.1/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
+modernc.org/ccgo/v4 v4.34.6 h1:sBgfIwyN0TQ9C5hwIeuqyeAKyMWnbvj2fvpF4L11uzU=
+modernc.org/ccgo/v4 v4.34.6/go.mod h1:SZ8YcN9NG7XVsQYdm6jYBvi8PQP1qi+kqB6OhjqI3Fk=
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
@@ -82,8 +82,8 @@ modernc.org/gc/v3 v3.1.4 h1:2g65LGVSmFQrXeITAw97x7hCRvZFcyE1uDP+7Vng7JI=
modernc.org/gc/v3 v3.1.4/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
-modernc.org/libc v1.73.5 h1:G34rN/cRqL+zOUnrbz9uPq/+OxJ8/vzQ2CQwTJ42Wmw=
-modernc.org/libc v1.73.5/go.mod h1:+Aoyx4M0etg6GikzCrip1VtvAtUlMlo2Aq+GHwQSqOA=
+modernc.org/libc v1.74.3 h1:a4J+Z8aVaxPyjyxRAdJzw246PqpcFGvVPnfT/AuM5Ws=
+modernc.org/libc v1.74.3/go.mod h1:4H7h/MJ8wnjL8RAbp9v3OXgnk22X7MouHIhDbvP3gj4=
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
@@ -92,8 +92,8 @@ modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
-modernc.org/sqlite v1.53.0 h1:20WG8N9q4ji/dEqGk4uiI0c6OPjSeLTNYGFCc3+7c1M=
-modernc.org/sqlite v1.53.0/go.mod h1:xoEpOIpGrgT48H5iiyt/YXPCZPEzlfmfFwtk8Lklw8s=
+modernc.org/sqlite v1.54.0 h1:JCxR4qwkJvOaqAoYcgDoO25Nc+ROg6EJ2LfBVzdrgog=
+modernc.org/sqlite v1.54.0/go.mod h1:4ntCLuNmnH8+GNqjka1wNg7KJd5/Hi5FYp8K+XQ7GZw=
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
diff --git a/internal/app/version.go b/internal/app/version.go
index 4ab8023..4e95a6c 100644
--- a/internal/app/version.go
+++ b/internal/app/version.go
@@ -3,4 +3,4 @@ package app
// Version is the source of truth for the application version.
// In order to update it use the following command:
// go run ./cmd/bump-version [-major, -minor, -patch, 1.2.0]
-const Version = "1.4.2"
+const Version = "1.4.3"