feat: Multi-line editable regex patterns for categories (frontend-only, no backend changes)#868
Conversation
Greptile SummaryThis PR replaces the single-line regex input in
Confidence Score: 3/5The data round-trip is safe (split → edit → join preserves the backend string), but the modal UI misbehaves for any existing rule whose regex contains a pipe inside a character class, and the error-message display logic fires two conflicting messages simultaneously. Two distinct defects affect existing users: character-class pipes in stored regexes cause the edit modal to display corrupted, invalid-looking patterns on open; and any invalid pattern produces a red 'Invalid pattern(s)' message AND a yellow 'Pattern(s) X invalid' message at the same time, undermining the validation UX. The !important binding in the tree view means the intended colour for pattern codes will never apply. None of these corrupt persisted data, but the first two meaningfully degrade the editing experience for real inputs. src/util/validate.ts (splitRegexPipe character-class handling) and src/components/CategoryEditModal.vue (error-message display conditions) need the most attention before merging. Important Files Changed
Sequence DiagramsequenceDiagram
participant Backend as Backend Store (regex: string)
participant Split as splitRegexPipe()
participant UI as Pattern List UI
participant Join as joinRegexPipe()
Backend->>Split: "GitHub|Stack Overflow|vim"
Split->>UI: ["GitHub", "Stack Overflow", "vim"]
Note over UI: User adds/removes/edits patterns
UI->>Join: ["GitHub", "VS Code", "vim"]
Join->>Backend: "GitHub|VS Code|vim"
Note over Split: ⚠️ [a-z|0-9] is split as ["[a-z", "0-9]"] (char classes ignored)
Reviews (1): Last reviewed commit: "feat: frontend multi regex rule for cate..." | Re-trigger Greptile |
| const parts: string[] = []; | ||
| let depth = 0; // paren-nesting depth — skip | when inside a group | ||
| let escaping = false; // simple \-escape for \| | ||
| let start = 0; | ||
| for (let i = 0; i < regex.length; i++) { | ||
| const ch = regex[i]; | ||
| if (escaping) { | ||
| escaping = false; | ||
| continue; | ||
| } | ||
| if (ch === '\\') { | ||
| escaping = true; | ||
| continue; | ||
| } | ||
| if (ch === '(') { | ||
| depth++; | ||
| continue; | ||
| } | ||
| if (ch === ')') { | ||
| if (depth > 0) depth--; | ||
| continue; | ||
| } | ||
| if (ch === '|' && depth === 0) { | ||
| parts.push(regex.slice(start, i)); | ||
| start = i + 1; | ||
| } | ||
| } |
There was a problem hiding this comment.
Character classes (
[...]) are not tracked, so a pipe inside one (e.g. [a-z|0-9]) triggers a split at depth 0 and produces ["[a-z", "0-9]"] — two syntactically broken patterns. When a user re-opens a modal for a rule that contains such a pattern, they'll see false validation errors, and the intended regex is visually mangled (though the round-trip through joinRegexPipe happens to reconstruct the original string on save). Adding an inClass flag that mirrors the depth logic for ( / ) fixes this.
| const parts: string[] = []; | |
| let depth = 0; // paren-nesting depth — skip | when inside a group | |
| let escaping = false; // simple \-escape for \| | |
| let start = 0; | |
| for (let i = 0; i < regex.length; i++) { | |
| const ch = regex[i]; | |
| if (escaping) { | |
| escaping = false; | |
| continue; | |
| } | |
| if (ch === '\\') { | |
| escaping = true; | |
| continue; | |
| } | |
| if (ch === '(') { | |
| depth++; | |
| continue; | |
| } | |
| if (ch === ')') { | |
| if (depth > 0) depth--; | |
| continue; | |
| } | |
| if (ch === '|' && depth === 0) { | |
| parts.push(regex.slice(start, i)); | |
| start = i + 1; | |
| } | |
| } | |
| const parts: string[] = []; | |
| let depth = 0; // paren-nesting depth — skip | when inside a group | |
| let inClass = false; // inside a character class [...] — | is literal here | |
| let escaping = false; // simple \-escape for \| | |
| let start = 0; | |
| for (let i = 0; i < regex.length; i++) { | |
| const ch = regex[i]; | |
| if (escaping) { | |
| escaping = false; | |
| continue; | |
| } | |
| if (ch === '\\') { | |
| escaping = true; | |
| continue; | |
| } | |
| if (ch === '[' && !inClass) { | |
| inClass = true; | |
| continue; | |
| } | |
| if (ch === ']' && inClass) { | |
| inClass = false; | |
| continue; | |
| } | |
| if (inClass) continue; // any character inside [...] is literal | |
| if (ch === '(') { | |
| depth++; | |
| continue; | |
| } | |
| if (ch === ')') { | |
| if (depth > 0) depth--; | |
| continue; | |
| } | |
| if (ch === '|' && depth === 0) { | |
| parts.push(regex.slice(start, i)); | |
| start = i + 1; | |
| } | |
| } |
| div.text-danger(v-if="!validPattern") Invalid pattern(s) | ||
| div.text-warning(v-if="validPattern && broad_pattern && !patternErrors.length") Pattern(s) too broad | ||
| div.text-warning(v-if="patternErrors.length > 0") | ||
| | Pattern(s) {{ patternErrors.join(', ') }} invalid |
There was a problem hiding this comment.
Duplicate error messages shown simultaneously
Both !validPattern (red "Invalid pattern(s)") and patternErrors.length > 0 (yellow "Pattern(s) X invalid") fire for exactly the same condition — any invalid entry makes validPattern false and also populates patternErrors. A user typing a broken regex sees both messages at once, which is redundant and visually contradictory (red danger + yellow warning for the same fact). The generic red message should only show when patternErrors is empty but validation still fails (i.e. !validPattern && !patternErrors.length).
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| span(v-if="_class.rule.type === 'regex'") | ||
| | Rule ({{_class.rule.type}}): | ||
| div.rule-item(v-for="(pat, indx) in splitRegex(_class.rule.regex)" :key="indx") | ||
| code(:style='{ color: "#d63384 !important" }') {{ pat }} |
There was a problem hiding this comment.
!important inside a Vue :style object binding is silently ignored by browsers. Vue sets individual CSS properties via element.style.setProperty(prop, value) where the value "#d63384 !important" is treated as an invalid value and discarded, so the colour override will never take effect. Use a scoped CSS rule or the string form of :style if you need !important.
| code(:style='{ color: "#d63384 !important" }') {{ pat }} | |
| code.rule-code {{ pat }} |
| export function validatePatternList(patterns: string[]): { | ||
| allValid: boolean; | ||
| results: { valid: boolean; broad: boolean }[]; | ||
| } { | ||
| const results = patterns | ||
| .filter(p => (p || '').trim().length > 0) | ||
| .map(p => ({ | ||
| valid: validateRegex(p.trim()), | ||
| broad: isRegexBroad(p.trim()), | ||
| })); | ||
| return { | ||
| allValid: results.every(r => r.valid), | ||
| results, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Returns true if *any* pattern in the list is overly broad. | ||
| */ | ||
| export function isPatternListBroad(patterns: string[]): boolean { | ||
| return patterns.some(p => isRegexBroad(p)); | ||
| } |
There was a problem hiding this comment.
validatePatternList and isPatternListBroad are exported but never called
Both functions were introduced in this PR but the modal uses inline logic (validateSinglePattern, validPattern computed, broad_pattern computed) instead. They are dead code as written. Either wire them into the component to consolidate the validation logic, or remove them to keep the surface clean.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
Summary
The original single-line regex input creates poor UX when multiple match patterns are needed — users must repeatedly move the cursor to the line end to append new pipe-separated rules.
This PR implements multi-line list editing and vertical display for category regex patterns, all logic contained in frontend with zero backend schema, API, or storage changes.
The backend continues to store one pipe-delimited regex string (
pattern1|pattern2|pattern3); splitting/rejoining between string and pattern list happens entirely client-side.Preview
Old UI:


New UI:


Changes
Add utility functions for pipe split/join and pattern validation
splitRegexPipe(regex: string): string[]: Split stored pipe string into individual pattern listjoinRegexPipe(patterns: string[]): string: Filter empty entries then join array back to single pipe regexvalidatePatternList(patterns: string[]): boolean: Run regex syntax check for every entry in listisPatternListBroad(patterns: string[]): boolean: Check if patterns are overly broad wildcard matches (existing logic ported to list)Replace single-line regex text input with dynamic editable pattern list
UI: Add add-row button, delete per-row button for each regex entry; if only one pattern exists, the delete button for that entry is disabled to prevent empty rule state
On modal open: Call
splitRegexPipeto parse backend rule.regex into array for list renderingOn save/submit: Validate all patterns via
validatePatternList, filter blanks, usejoinRegexPipeto rebuild pipe string for backend payloadPer-line real-time regex syntax validation feedback matching original validation behavior
Rule display updated: Render each split pattern as separate line item
Each pattern renders on its own block line, matching list edit preview
Data Flow (No Backend Modifications)
Backend store: regex: "GitHub|Stack Overflow|vim"
Modal mount: splitRegexPipe("GitHub|Stack Overflow|vim") → ["GitHub", "Stack Overflow", "vim"]
User edits list (add/remove/edit individual patterns in UI)
Save trigger: Validate all patterns → filter empty strings → joinRegexPipe(array) → "GitHub|Stack Overflow|vim"
Send unchanged API payload shape ({ regex: string }) back to backend
Compatibility & Safety
Full backward compatibility with existing saved categories: old single regex strings split cleanly to one-item lists
Old clients / other frontends function normally with the same backend data
Matching logic unchanged: backend still evaluates one combined A|B|C regex exactly as before
No database migration, no API version bump, no server code edits required
Visual Notes
List items stack vertically, no inline crowding