Skip to content

Add CodeMirror support to Rules tab with syntax highlighting, line number, search and replace...#559

Open
gunir wants to merge 8 commits intoZenPrivacy:masterfrom
gunir:codemirror
Open

Add CodeMirror support to Rules tab with syntax highlighting, line number, search and replace...#559
gunir wants to merge 8 commits intoZenPrivacy:masterfrom
gunir:codemirror

Conversation

@gunir
Copy link

@gunir gunir commented Dec 20, 2025

What does this PR do?

CodeMirror is a versatile, modern text editor component built specifically for the web, designed to handle complex editing tasks that go far beyond the capabilities of a standard HTML <textarea>. While a plain textarea is part of the basic browser toolkit, CodeMirror 6 (the version we implemented) is a complete rewrite focused on modularity, accessibility, and high performance.

Here is a comparison between CodeMirror and the previous plain textarea used in your Vite/Wails project:

Performance and Rendering

Virtualization: The standard Vite textarea renders the entire text buffer at once. If you have 10,000 lines of rules, the browser must manage all 10,000 lines in memory and layout. CodeMirror uses virtualization, meaning it only creates DOM elements for the few dozen lines currently visible on your screen, making scrolling instant regardless of file size.

Update Cycle: Your previous Vite setup used a "controlled component" pattern where every keystroke forced React to re-process the entire string, causing the lag you observed. CodeMirror manages its own internal state and only notifies React when changes are finished, bypassing the expensive React render loop.

Editing Features

Syntax Highlighting: A plain textarea only supports a single color for all text. CodeMirror allows for sophisticated parsing, which we used to highlight your uBlockOrigin comments in gray and syntax symbols in distinct colors.

Developer Tools: CodeMirror includes built-in support for professional features that a textarea lacks, such as native line numbers, specialized search-and-replace panels, and bracket matching.

Code-Aware Logic: Unlike the plain textarea, CodeMirror understands the document structure. This allowed us to implement the "Format Rules" feature to programmatically sort and deduplicate your rules while maintaining the integrity of your comments.

Layout and Feel

Scroll Behavior: The previous textarea was subject to browser-level "smooth scrolling" and momentum interpolation, which contributed to a sluggish feel in the Wails webview. CodeMirror provides direct control over the scroll container, allowing us to force an "instant" response to mouse and keyboard input.

Isolation: CodeMirror uses CSS containment to ensure that internal changes to the text do not trigger expensive layout recalculations for the rest of your Wails application UI.

How did you verify your code works?

Check this image:

image

What are the relevant issues?

No, just improvement

This PR add CodeMirror support to improve performance, improve rule editing experience

Signed-off-by: Gunir <134402102+gunir@users.noreply.github.com>
Update index.css - Add CodeMirror

Signed-off-by: Gunir <134402102+gunir@users.noreply.github.com>
@coderabbitai
Copy link

coderabbitai bot commented Dec 20, 2025

Walkthrough

Replaces the textarea-based Rules editor with a CodeMirror 6 editor, adds adblock syntax highlighting, search, line numbers, oneDark theme, debounced GetRules/SetRules persistence, a Format action preserving comment lines, readOnly toggling tied to proxy state, and CSS/layout updates with an initial loading state.

Changes

Cohort / File(s) Summary
CSS: Rules layout & CodeMirror styling
frontend/src/Rules/index.css
Update container sizing/overflow; rename .rules__help-button.rules__header and add flex header layout; replace textarea styles with .rules__hint, .rules__tooltip, .rules__editor-container; add CodeMirror editor/gutters/line-number and search-panel theming; add .rules--loading centering.
Rules component: editor rewrite
frontend/src/Rules/index.tsx
Replace textarea with CodeMirror 6: initialization, updates, cleanup; add custom adblock language/highlighter, oneDark theme, line numbers, search, history, wrap, keymaps; implement debounced GetRules/SetRules persistence and initial loading gating; add handleFormat (dedupe/sort while preserving ! comment lines); readOnly via a Compartment reacting to isProxyRunning; header with Help and Format buttons and a loading spinner.

Sequence Diagram

sequenceDiagram
    participant User
    participant RulesComp as Rules Component
    participant CM as CodeMirror
    participant API as GetRules/SetRules
    participant State as App State

    User->>RulesComp: mount
    RulesComp->>API: fetch initial rules
    API-->>RulesComp: return rules
    RulesComp->>CM: initialize with rules
    RulesComp->>State: subscribe isProxyRunning

    User->>CM: edit
    CM-->>RulesComp: onChange event
    RulesComp->>RulesComp: debounce update
    RulesComp->>API: SetRules (persist)
    API-->>RulesComp: ack

    State->>RulesComp: isProxyRunning changes
    RulesComp->>CM: update readOnly (Compartment)

    User->>RulesComp: click Format
    RulesComp->>RulesComp: dedupe & sort (preserve !comments)
    RulesComp->>CM: apply formatted content
    RulesComp->>API: SetRules (persist)
    API-->>RulesComp: ack
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

  • Review custom adblock language/highlighter and CodeMirror extension composition.
  • Verify debounce timing, initial loading gating, and persistence edge cases.
  • Confirm handleFormat preserves comment lines and correctly deduplicates/sorts.
  • Check readOnly Compartment updates on isProxyRunning and proper cleanup on unmount.
  • Validate CSS layout and search-panel theming across view sizes.

Possibly related issues

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding CodeMirror support with syntax highlighting, line numbers, and search-and-replace features to the Rules tab.
Description check ✅ Passed The description provides detailed explanations of changes and verification via screenshot, but lacks testing strategy and misses the structured template format.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
frontend/src/Rules/index.css (1)

42-59: Consider using CSS variables instead of hardcoded colors.

The CodeMirror styling uses hardcoded hex colors and multiple !important declarations. This approach:

  • Tightly couples the editor to the One Dark theme
  • Makes it difficult to support theme switching or design system updates
  • Could cause maintenance issues with excessive !important usage

Consider defining CSS variables for these colors at the root level and referencing them here for better maintainability and theme flexibility.

🔎 Proposed refactor using CSS variables

Define variables (e.g., in a root/theme file):

:root {
  --editor-bg: #282c34;
  --editor-gutter-bg: #21252b;
  --editor-border: #333;
  --editor-line-number: #606873;
  --editor-text: #abb2bf;
  --editor-input-bg: #21252b;
  --editor-search-border: #181a1f;
}

Then update this file:

 /* CodeMirror UI Enhancements */
-.cm-editor { height: 100% !important; }
-.cm-gutters { background-color: #21252b !important; border-right: 1px solid #333 !important; }
-.cm-lineNumbers .cm-gutterElement { padding: 0 8px !important; color: #606873 !important; }
+.cm-editor { height: 100%; }
+.cm-gutters { background-color: var(--editor-gutter-bg); border-right: 1px solid var(--editor-border); }
+.cm-lineNumbers .cm-gutterElement { padding: 0 8px; color: var(--editor-line-number); }

 /* Search Panel Styling to match One Dark */
 .cm-search {
-  background: #282c34 !important;
-  color: #abb2bf !important;
-  border-bottom: 1px solid #181a1f !important;
-  padding: 8px !important;
+  background: var(--editor-bg);
+  color: var(--editor-text);
+  border-bottom: 1px solid var(--editor-search-border);
+  padding: 8px;
 }
 .cm-search input { 
-  background: #21252b !important; 
-  color: white !important; 
-  border: 1px solid #181a1f !important; 
+  background: var(--editor-input-bg); 
+  color: white; 
+  border: 1px solid var(--editor-search-border); 
   border-radius: 3px; 
 }
frontend/src/Rules/index.tsx (1)

31-50: LGTM - Basic adblock syntax highlighting is functional.

The custom highlighter correctly identifies comments (lines starting with !), exception rules (@@), and domain anchors (||). The implementation is straightforward and appropriate for the PR's goals.

For future enhancement, consider adding patterns for other common adblock syntax elements like | (pipe), ^ (separator), $ (options), and wildcards (*) to provide more comprehensive highlighting.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ca839bd and 537e891.

📒 Files selected for processing (2)
  • frontend/src/Rules/index.css (1 hunks)
  • frontend/src/Rules/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/Rules/index.tsx (3)
frontend/src/context/ProxyStateContext.tsx (1)
  • useProxyState (30-36)
frontend/wailsjs/go/cfg/Config.js (2)
  • GetRules (49-51)
  • SetRules (109-111)
frontend/wailsjs/runtime/runtime.js (1)
  • BrowserOpenURL (176-178)
🔇 Additional comments (11)
frontend/src/Rules/index.css (4)

3-9: LGTM - Container layout properly configured for CodeMirror.

The changes to full height/width with overflow control align well with the CodeMirror integration requirements mentioned in the PR objectives.


11-17: LGTM - Header flex layout is well structured.

The flex-shrink: 0 is a good defensive measure to prevent the header from collapsing when the editor content grows.


19-31: LGTM - Hint and tooltip styling is appropriate.

The monospace font for the hint text aligns well with the editor's aesthetic.


33-40: LGTM - Editor container properly configured.

The overflow: hidden is particularly important for controlling CodeMirror's scroll behavior within the Wails webview, as mentioned in the PR objectives.

frontend/src/Rules/index.tsx (7)

1-27: LGTM - Imports are well-organized and comprehensive.

The CodeMirror imports cover all necessary modules for the editor functionality described in the PR objectives.


52-65: LGTM - Component state and initialization are well-structured.

The use of initialRules with null initial state enables proper loading state handling. The Compartment memoization is appropriate for managing dynamic readonly state.


67-71: LGTM - Debounced persistence is properly implemented.

The 500ms debounce is a good balance between responsiveness and backend load. The processing logic correctly filters out empty lines.


73-97: LGTM - Format logic is correct and efficient.

The implementation properly:

  • Deduplicates using Set
  • Preserves comments at the top
  • Sorts rules alphabetically with localeCompare
  • Updates the editor in a single transaction

132-138: LGTM - Readonly state synchronization is correctly implemented.

The Compartment pattern is the proper approach for dynamically reconfiguring editor state in CodeMirror 6.


163-173: LGTM - Tooltip and editor container properly configured.

The tooltip correctly disables when the proxy is not running, and the editor container ref is properly managed.


99-130: The editor initialization effect is properly structured with correct CodeMirror 6 configuration.

The useEffect correctly:

  • Guards against duplicate initialization with editorRef.current && !viewRef.current checks
  • Configures comprehensive CodeMirror extensions including search with the correct top: true parameter
  • Integrates the update listener to persist filter changes via setFilters
  • Cleans up the editor view on unmount

The single [initialRules] dependency is appropriate since the effect should run only once after initial data load.

Copy link
Member

@anfragment anfragment left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please see the failing builds and the frontend linter's output (automatically fixable with npm run lint -- --fix).

Copy link
Author

@gunir gunir left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, I'll look into it later!

.rules--loading {
  display: flex;
  align-items: center;
  justify-content: center;
  height: 100%;
  width: 100%;
}

Signed-off-by: Gunir <134402102+gunir@users.noreply.github.com>
Signed-off-by: Gunir <134402102+gunir@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (1)
frontend/src/Rules/index.tsx (1)

140-142: Loading state CSS class issue already flagged.

This issue was already raised in previous review comments, and you acknowledged it. The .rules--loading CSS class needs to be added to frontend/src/Rules/index.css.

🧹 Nitpick comments (1)
frontend/src/Rules/index.tsx (1)

34-50: Consider optimizing the tokenizer for better performance.

The catch-all case at line 42 uses stream.next() which advances only one character at a time. For long sequences of regular characters (not comments, @@, or ||), this calls the tokenizer once per character. Consider consuming all non-special characters in a single call:

if (stream.match(/^[^!@|]+/)) return null;

This would reduce the number of tokenizer invocations for typical adblock rules.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b3c8093 and 997d3e8.

📒 Files selected for processing (1)
  • frontend/src/Rules/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/Rules/index.tsx (3)
frontend/src/context/ProxyStateContext.tsx (1)
  • useProxyState (30-36)
frontend/wailsjs/go/cfg/Config.js (2)
  • GetRules (49-51)
  • SetRules (109-111)
frontend/wailsjs/runtime/runtime.js (1)
  • BrowserOpenURL (176-178)
🔇 Additional comments (5)
frontend/src/Rules/index.tsx (5)

73-97: Format logic looks solid.

The deduplication and sorting logic correctly:

  • Removes duplicates using a Set
  • Preserves comment lines (starting with !) at the top
  • Sorts remaining rules alphabetically
  • Dispatches the changes to replace the entire document

99-130: Editor initialization and lifecycle management are correct.

The effect properly:

  • Waits for initial rules to load before creating the editor
  • Initializes CodeMirror with appropriate extensions (line numbers, search, syntax highlighting, etc.)
  • Wires up the debounced persistence via updateListener
  • Cleans up by destroying the view on unmount

The dependency array [initialRules] is intentionally narrow since other captured values (setFilters, readOnlyCompartment) are stable references, and isProxyRunning changes are handled by a dedicated effect.


132-138: ReadOnly state management is correct.

The effect properly reconfigures the editor's readonly state in response to isProxyRunning changes. The inclusion of readOnlyCompartment in the dependency array is technically redundant (it's stable from useMemo), but harmless.


144-175: UI rendering structure is well organized.

The component structure is clear:

  • Header with Help and Format buttons using ButtonGroup
  • Search hint for discoverability
  • Tooltip that explains readonly state when proxy is running
  • Editor container properly ref'd and styled with conditional disabled class

110-110: The top: true parameter is not a valid option for the search() function.

The search() function's only documented configuration option is createPanel, which is a callback function that returns an object with { dom, top: true }. The top property controls panel positioning and belongs in the return value of the createPanel callback, not as a direct parameter to search().

The current code search({ top: true }) either has no effect or may cause unexpected behavior. Either remove the parameter to use the default search panel, or implement a proper createPanel callback if custom panel positioning is needed.

Likely an incorrect or invalid review comment.

Signed-off-by: Gunir <134402102+gunir@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
frontend/src/Rules/index.tsx (1)

142-144: Missing CSS class for loading state.

As noted in previous reviews, the .rules--loading class is not defined in the CSS file. Please add the CSS rule to properly style the loading spinner.

🧹 Nitpick comments (1)
frontend/src/Rules/index.tsx (1)

97-131: Remove isProxyRunning from the effect dependencies.

Including isProxyRunning in the dependencies causes the editor to be destroyed and recreated whenever the proxy state changes. This is unnecessary because the read-only state is already handled separately via compartment reconfiguration in the effect at lines 133-140. Removing it will prevent unnecessary editor recreation and improve performance.

🔎 Proposed fix
     return () => {
       if (viewRef.current) {
         viewRef.current.destroy();
         viewRef.current = null;
       }
     };
-  }, [initialRules, isProxyRunning, readOnlyCompartment, setFilters]);
+  }, [initialRules, readOnlyCompartment, setFilters]);
📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 997d3e8 and 02c8190.

📒 Files selected for processing (1)
  • frontend/src/Rules/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/Rules/index.tsx (3)
frontend/src/context/ProxyStateContext.tsx (1)
  • useProxyState (30-36)
frontend/wailsjs/go/cfg/Config.js (2)
  • GetRules (49-51)
  • SetRules (109-111)
frontend/wailsjs/runtime/runtime.js (1)
  • BrowserOpenURL (176-178)
🔇 Additional comments (2)
frontend/src/Rules/index.tsx (2)

71-95: LGTM! Format logic is well-implemented.

The deduplication and sorting logic correctly handles comment preservation and uses locale-aware comparison for internationalized sorting.


133-140: LGTM! Efficient read-only state management.

Using compartment reconfiguration to update the read-only state is the correct approach and avoids unnecessary editor recreation.

Signed-off-by: Gunir <134402102+gunir@users.noreply.github.com>
Signed-off-by: Gunir <134402102+gunir@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
frontend/src/Rules/index.tsx (2)

148-150: The missing .rules--loading CSS class was already flagged in a previous review. Please ensure it's added to index.css as discussed.


169-169: The hardcoded search hint was already flagged in a previous review for translation. Additionally, consider detecting the platform to show the correct modifier key (Cmd on macOS, Ctrl on Windows/Linux) for better UX.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02c8190 and 9cff602.

📒 Files selected for processing (1)
  • frontend/src/Rules/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/Rules/index.tsx (3)
frontend/src/context/ProxyStateContext.tsx (1)
  • useProxyState (30-36)
frontend/wailsjs/go/cfg/Config.js (2)
  • GetRules (49-51)
  • SetRules (109-111)
frontend/wailsjs/runtime/runtime.js (1)
  • BrowserOpenURL (176-178)
🔇 Additional comments (3)
frontend/src/Rules/index.tsx (3)

139-146: LGTM!

The compartment reconfiguration pattern is the correct approach for updating CodeMirror state dynamically without recreating the editor.


24-43: Nice adblock syntax highlighter implementation.

The StreamLanguage definition cleanly handles comment lines, exception markers (@@), and domain anchors (||). The tokenTable mapping to lezer highlight tags integrates well with the oneDark theme.


77-101: Format logic is well-implemented.

The deduplication via Set, comment-first sorting, and use of localeCompare for stable ordering are all correct. The null check on viewRef.current is a good defensive practice.

Signed-off-by: Gunir <134402102+gunir@users.noreply.github.com>
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
frontend/src/Rules/index.tsx (1)

66-75: LGTM! Error handling in place.

The debounced save callback now includes proper error handling (lines 67-74) that logs failures. The comment on line 73 suggests adding a user-facing toast notification, which would improve UX but is not critical since the error is logged.

If you'd like, I can help implement a toast notification to surface save failures to users.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 49a5444 and 3759995.

📒 Files selected for processing (1)
  • frontend/src/Rules/index.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
frontend/src/Rules/index.tsx (3)
frontend/src/context/ProxyStateContext.tsx (1)
  • useProxyState (30-36)
frontend/wailsjs/go/cfg/Config.js (2)
  • GetRules (49-51)
  • SetRules (109-111)
frontend/wailsjs/runtime/runtime.js (1)
  • BrowserOpenURL (176-178)
🔇 Additional comments (4)
frontend/src/Rules/index.tsx (4)

1-21: LGTM! Clean CodeMirror 6 integration setup.

All imports are correctly structured for the CodeMirror 6 editor integration. The BlueprintJS components, React hooks, and CodeMirror modules are properly imported.


24-43: LGTM! Well-implemented custom syntax highlighter.

The adblock language definition correctly uses CodeMirror's StreamLanguage.define pattern with proper token parsing and Lezer tag mapping. The comment detection (lines starting with "!"), keyword highlighting ("@@"), and operator highlighting ("||") follow the correct stream-based parsing approach.


46-51: LGTM! Proper state and ref initialization.

The state setup correctly uses useMemo to create a stable Compartment instance (line 51), which is the recommended pattern for dynamic CodeMirror configuration. All refs and state are properly typed.


54-64: LGTM! Error handling properly implemented.

The fetch effect now includes proper error handling (lines 56-62) that prevents infinite loading by falling back to an empty editor if GetRules() fails. This correctly addresses the issue raised in previous reviews.

Copy link
Member

@anfragment anfragment left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gunir Please take a look at the remaining linting and build errors.

The PR should also include the updated package.json and package-lock.json.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants