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/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()); 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([]); + }); +});