From b888f40af3d943cde42a78cca5cc2850a79ebce6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zge=20G=C3=BCner?= Date: Thu, 13 Aug 2026 10:35:35 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(columns):=20autoDetectSpans=20?= =?UTF-8?q?=E2=80=94=20tokenise=20a=20line=20into=20typed=20column=20spans?= =?UTF-8?q?=20(auto-analyze=20core)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pure, tested tokenizer for the 'auto-analyze a line' flow: split on whitespace at bracket/quote depth 0, keeping [..]/(..)/{..}/<..>/quotes whole, each token β†’ a column (col1..). Feeds the tested compilePaint (which types each token). +3 tests. Renderer button (πŸ” Auto-detect columns) wiring lands next. tsc 0 Β· 631 tests. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/main/columnPattern.ts | 26 ++++++++++++++++++++++++++ src/tests/columnPattern.test.ts | 26 +++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/main/columnPattern.ts b/src/main/columnPattern.ts index b570da7..851afbf 100644 --- a/src/main/columnPattern.ts +++ b/src/main/columnPattern.ts @@ -217,6 +217,32 @@ function compileRawRegex(pattern: string, flags: string): CompiledColumnPattern return { regex: pattern, flags, fields: ['match'], named: false }; } +export interface DetectedSpan { start: number; end: number; name: string } + +// Auto-detect column spans in a sample line: split on whitespace at bracket/quote depth 0, +// keeping [..], (..), {..}, <..>, "..", '..' groups whole. Each token becomes a column +// (col1, col2…). Simple + solid β€” the caller feeds these to compilePaint, which TYPES each +// token (date/int/word…); the user then names/tweaks in the Column Layout panel. +export function autoDetectSpans(line: string): DetectedSpan[] { + const spans: DetectedSpan[] = []; + const closerFor: Record = { '[': ']', '(': ')', '{': '}', '<': '>' }; + let start = -1; + let quote = ''; + const stack: string[] = []; + const flush = (i: number) => { if (start >= 0) { spans.push({ start, end: i, name: `col${spans.length + 1}` }); start = -1; } }; + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (quote) { if (ch === quote) quote = ''; continue; } // inside a quote β†’ keep going + if (ch === '"' || ch === "'") { if (start < 0) start = i; quote = ch; continue; } + if (closerFor[ch]) { if (start < 0) start = i; stack.push(closerFor[ch]); continue; } + if (stack.length && ch === stack[stack.length - 1]) { if (start < 0) start = i; stack.pop(); continue; } + if (/\s/.test(ch) && stack.length === 0) { flush(i); continue; } // whitespace at depth 0 β†’ split + if (start < 0) start = i; + } + flush(line.length); + return spans; +} + /** Compile any spec to a single named/numbered-capture regex + ordered fields. */ export function compileColumnPattern(spec: ColumnPatternSpec): CompiledColumnPattern { const flags = spec.flags || ''; diff --git a/src/tests/columnPattern.test.ts b/src/tests/columnPattern.test.ts index 46d42e0..5fcba91 100644 --- a/src/tests/columnPattern.test.ts +++ b/src/tests/columnPattern.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest'; -import { compileColumnPattern, makeColumnExtractor, refinePaintPattern, ColumnPatternSpec } from '../main/columnPattern'; +import { compileColumnPattern, makeColumnExtractor, refinePaintPattern, autoDetectSpans, ColumnPatternSpec } from '../main/columnPattern'; function extract(spec: ColumnPatternSpec, line: string): string[] | null { return makeColumnExtractor(compileColumnPattern(spec))(line); @@ -142,3 +142,27 @@ describe('refinePaintPattern β€” refine from data (peel constant wrappers)', () expect(makeColumnExtractor(c)('foo -> ok')).toEqual(['foo']); // 1 space also matches (\\s+) }); }); + +describe('autoDetectSpans β€” tokenise a line into column spans', () => { + const toks = (line: string) => autoDetectSpans(line).map(s => line.slice(s.start, s.end)); + + it('splits on whitespace and keeps [..] and quotes whole', () => { + expect(toks('2024-01-02 INFO [Auth Service] user=bob')) + .toEqual(['2024-01-02', 'INFO', '[Auth Service]', 'user=bob']); + expect(toks('a "hello world" b')).toEqual(['a', '"hello world"', 'b']); + }); + + it('names columns col1..colN and the spans feed compilePaint (typed)', () => { + const line = '2024-01-02 42'; + const spans = autoDetectSpans(line); + expect(spans.map(s => s.name)).toEqual(['col1', 'col2']); + const c = compileColumnPattern({ mode: 'paint', sample: line, spans }); + expect(c.regex).toContain('\\d{4}-\\d{2}-\\d{2}'); // date typed + expect(c.regex).toContain('-?\\d+'); // int typed + }); + + it('handles empty / whitespace-only lines', () => { + expect(autoDetectSpans('')).toEqual([]); + expect(autoDetectSpans(' ')).toEqual([]); + }); +}); From 101db812d9fd111af992c9a55c83df6156f9b2dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=96zge=20G=C3=BCner?= Date: Thu, 13 Aug 2026 10:39:39 +0200 Subject: [PATCH 2/2] =?UTF-8?q?feat(columns):=20=F0=9F=94=8D=20Auto-detect?= =?UTF-8?q?=20columns=20button=20=E2=80=94=20auto-analyze=20a=20line=20int?= =?UTF-8?q?o=20the=20panel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The 'auto-analyze a line' flow (replaces the hidden Refine-from-data as the star): click 'πŸ” Auto-detect columns' in the Column Layouts builder β†’ it tokenises the sample line (mirror of the tested autoDetectSpans: whitespace split, brackets/quotes kept whole) into paint spans β†’ the tested compilePaint TYPES each (date/int/word…, borders literal) β†’ the draft columns land in the panel to rename/tweak, then Use as columns. The data-driven wrapper generalisation still folds in via the preview, but now it's part of an EXPLICIT analyze action, not hidden. tsc 0 Β· 631 tests Β· bundle. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/renderer/index.html | 1 + src/renderer/renderer.ts | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/renderer/index.html b/src/renderer/index.html index 8c81655..29bcbd7 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -486,6 +486,7 @@

LOGAN

+ diff --git a/src/renderer/renderer.ts b/src/renderer/renderer.ts index f1791b5..f505064 100644 --- a/src/renderer/renderer.ts +++ b/src/renderer/renderer.ts @@ -15652,6 +15652,44 @@ async function patcolPullSample(): Promise { } catch { /* ignore */ } } +// MIRROR of autoDetectSpans() in src/main/columnPattern.ts (tested there) β€” keep in sync. +function autoDetectSpansLocal(line: string): PatcolSpan[] { + const spans: PatcolSpan[] = []; + const closerFor: Record = { '[': ']', '(': ')', '{': '}', '<': '>' }; + let start = -1, quote = ''; + const stack: string[] = []; + const flush = (i: number) => { if (start >= 0) { spans.push({ start, end: i, name: `col${spans.length + 1}` }); start = -1; } }; + for (let i = 0; i < line.length; i++) { + const ch = line[i]; + if (quote) { if (ch === quote) quote = ''; continue; } + if (ch === '"' || ch === "'") { if (start < 0) start = i; quote = ch; continue; } + if (closerFor[ch]) { if (start < 0) start = i; stack.push(closerFor[ch]); continue; } + if (stack.length && ch === stack[stack.length - 1]) { if (start < 0) start = i; stack.pop(); continue; } + if (/\s/.test(ch) && stack.length === 0) { flush(i); continue; } + if (start < 0) start = i; + } + flush(line.length); + return spans; +} + +// "πŸ” Auto-detect columns": analyse a sample line β†’ paint spans (one per token), which the +// tested compilePaint then TYPES (date/int/word…). The draft lands in the panel to name/tweak. +async function patcolAutoDetect(): Promise { + const sampleInput = document.getElementById('patcol-sample-input') as HTMLInputElement | null; + if (!sampleInput || !state.filePath) return; + if (!sampleInput.value.trim()) { + const ln = state.selectedLine != null ? state.selectedLine : (state.visibleStartLine ?? 0); + try { const r = await window.api.getLines(ln, 1); const t = r?.lines?.[0]?.text || ''; if (t) sampleInput.value = t; } catch { /* ignore */ } + } + const line = sampleInput.value; + if (!line.trim()) { showToast('No sample line to analyze β€” put the cursor on a line first'); return; } + patcolSetMode('paint'); + patcolSpans = autoDetectSpansLocal(line); + patcolRenderPaint(); + patcolPreviewSoon(); + showToast(`Auto-detected ${patcolSpans.length} columns β€” rename/adjust, then Use as columns`); +} + function patcolSetMode(mode: PatcolMode): void { patcolMode = mode; document.querySelectorAll('.patcol-mode-btn').forEach((b) => { @@ -23249,6 +23287,7 @@ function init(): void { if (mode) patcolSetMode(mode); })); document.getElementById('btn-patcol-sample')?.addEventListener('click', patcolPullSample); + document.getElementById('btn-patcol-autodetect')?.addEventListener('click', () => { void patcolAutoDetect(); }); document.getElementById('btn-patcol-copy')?.addEventListener('click', patcolCopyRegex); document.getElementById('btn-patcol-save')?.addEventListener('click', patcolSavePattern); document.getElementById('btn-patcol-apply')?.addEventListener('click', () => patcolUseAsColumns());