Skip to content

feat(MarkdownViewer): configurable keyboard shortcut to toggle TOC visibility with animation#1935

Merged
emako merged 6 commits intomasterfrom
copilot/add-configurable-keyboard-shortcuts-toc
May 9, 2026
Merged

feat(MarkdownViewer): configurable keyboard shortcut to toggle TOC visibility with animation#1935
emako merged 6 commits intomasterfrom
copilot/add-configurable-keyboard-shortcuts-toc

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 9, 2026

No keyboard shortcut existed to toggle the Markdown viewer's Table of Contents panel. This adds Ctrl+Shift+L (matching Typora) as the default, fully configurable via the plugin config file.

Changes

md2html.html

  • No-flash initial state: Head script reads tocVisible from localStorage before paint and sets --toc-width, --toc-min-width, --toc-overflow-y CSS variables immediately, so a previously-collapsed TOC doesn't flicker on load
  • Smooth animation: #toc gains transition: width 0.3s ease; min-width and overflow-y now use CSS variables so they can be overridden at paint time
  • toggleToc(): Collapses/expands width with a 300ms slide animation (TOC_TRANSITION_MS constant keeps CSS and JS duration in sync); state persisted in localStorage
  • matchesShortcut(e, shortcut): Parses shortcut strings like "Ctrl+Shift+L" — supports Ctrl, Shift, Alt modifiers, case-insensitive key name, filters empty/malformed parts
  • Keyboard handler: TOC toggle fires before RTL shortcuts to avoid conflicts; no-op when document has fewer than 2 headings (TOC already hidden)
  • Resize guard: Cached tocIsVisible boolean (vs. repeated localStorage.getItem) gates the resize-handle hit-test, preventing accidental resize interaction on a collapsed TOC

MarkdownPanel.cs

Reads ToggleTocKey from QuickLook.Plugin.MarkdownViewer.config via SettingHelper.Get (default Ctrl+Shift+L) and injects it as {{tocShortcut}} into the HTML template.

OPTIONS.md

New QuickLook.Plugin.MarkdownViewer.config options section documenting ToggleTocKey.

Configuration

<!-- %APPDATA%\pooi.moe\QuickLook\QuickLook.Plugin.MarkdownViewer.config -->
<?xml version="1.0" encoding="utf-8"?>
<Settings>
  <ToggleTocKey>Ctrl+Shift+L</ToggleTocKey>
</Settings>

Supported modifiers: Ctrl, Shift, Alt. Key name is matched case-insensitively against KeyboardEvent.key (e.g. L, F1, Escape).

Summary by Sourcery

Add a configurable keyboard shortcut to toggle the Markdown viewer’s Table of Contents panel with smooth animated show/hide behavior and persist its state across sessions.

New Features:

  • Introduce a host-configurable keyboard shortcut (default Ctrl+Shift+L) to toggle the Markdown viewer’s TOC visibility.
  • Expose a global JavaScript function to allow the host application to programmatically toggle the TOC panel.

Enhancements:

  • Preserve TOC visibility and width via localStorage to avoid flicker and layout shifts on load.
  • Animate TOC show/hide transitions and disable resizing when the TOC is collapsed for a smoother UI experience.

Documentation:

  • Document the new ToggleTocKey option in OPTIONS.md with usage, defaults, and examples.

Copilot AI changed the title [WIP] Add configurable keyboard shortcuts to toggle TOC visibility feat(MarkdownViewer): configurable keyboard shortcut to toggle TOC visibility with animation May 9, 2026
Copilot AI requested a review from emako May 9, 2026 03:10
Copilot finished work on behalf of emako May 9, 2026 03:10
emako added 2 commits May 9, 2026 18:02
Ensure the table-of-contents overflow is explicitly set to "auto" after the transition to override any lingering --toc-overflow-y CSS variable, replacing the previous empty string assignment. Also expose toggleToc on window so the host application (e.g. C# via ExecuteScriptAsync) can invoke it programmatically.
Temporarily disable the TOC element's CSS transition while the user is dragging the resize handle so the width updates immediately; restore the transition when the drag ends. This change in md2html.html prevents delayed width updates during TOC resizing for a smoother drag experience.
@emako emako marked this pull request as ready for review May 9, 2026 10:07
Copilot AI review requested due to automatic review settings May 9, 2026 10:07
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 9, 2026

Reviewer's Guide

Adds a configurable keyboard shortcut (default Ctrl+Shift+L) to toggle the Markdown viewer’s Table of Contents visibility with a smooth animated collapse/expand, persists state and width in localStorage, guards resize behavior when collapsed, and wires the shortcut through plugin configuration and documentation.

Sequence diagram for Markdown TOC toggle keyboard shortcut handling

sequenceDiagram
  actor User
  participant MarkdownViewerWindow
  participant MarkdownViewerScript
  participant localStorage

  User->>MarkdownViewerWindow: Press Ctrl+Shift+L or configured shortcut
  MarkdownViewerWindow->>MarkdownViewerScript: keydown event
  MarkdownViewerScript->>MarkdownViewerScript: matchesShortcut(e, TOC_TOGGLE_SHORTCUT)
  alt Shortcut matches and count >= 2
    MarkdownViewerScript->>MarkdownViewerScript: toggleToc()
    alt TOC currently visible
      MarkdownViewerScript->>MarkdownViewerScript: set width 0px, minWidth 0px, overflowY hidden
      MarkdownViewerScript->>localStorage: setItem(tocVisible, false)
    else TOC currently hidden
      MarkdownViewerScript->>localStorage: getItem(tocWidth) or 178px
      MarkdownViewerScript->>MarkdownViewerScript: set width to saved width, minWidth 4px
      MarkdownViewerScript->>localStorage: setItem(tocVisible, true)
      MarkdownViewerScript->>MarkdownViewerScript: setTimeout(restore overflowY auto after TOC_TRANSITION_MS)
    end
  else Shortcut does not match
    MarkdownViewerScript->>MarkdownViewerScript: handle other shortcuts (e.g. RTL/LTR)
  end
Loading

Sequence diagram for injecting configurable TOC shortcut into Markdown HTML

sequenceDiagram
  participant QuickLookHost
  participant MarkdownPanel
  participant SettingHelper
  participant Template as md2html_html_template
  participant WebView

  QuickLookHost->>MarkdownPanel: GenerateMarkdownHtml(path)
  MarkdownPanel->>SettingHelper: Get(ToggleTocKey, Ctrl+Shift+L, QuickLook.Plugin.MarkdownViewer)
  SettingHelper-->>MarkdownPanel: tocShortcut string
  MarkdownPanel->>Template: Replace {{content}}, {{rtl}}, {{tocShortcut}}
  Template-->>MarkdownPanel: Final HTML
  MarkdownPanel-->>QuickLookHost: HTML string
  QuickLookHost->>WebView: Load HTML
  WebView->>WebView: TOC_TOGGLE_SHORTCUT = injected tocShortcut
Loading

Updated class diagram for MarkdownPanel TOC shortcut configuration

classDiagram
  class MarkdownPanel {
    +GenerateMarkdownHtml(path string) string
  }

  class SettingHelper {
    +Get(key string, defaultValue string, section string) string
  }

  MarkdownPanel ..> SettingHelper : uses for ToggleTocKey
Loading

File-Level Changes

Change Details Files
Implement animated TOC visibility toggling with persisted state and no-flash initial layout in the Markdown HTML template.
  • Read tocVisible and tocWidth from localStorage in a head script to set CSS variables before paint, avoiding flicker when TOC was previously collapsed.
  • Change #toc styling to use CSS variables for min-width and overflow-y and add a width transition to enable smooth sliding.
  • Introduce a TOC_TRANSITION_MS constant and a tocIsVisible flag, initializing overflow/min-width correctly for a previously-collapsed TOC.
  • Add a toggleToc() function that animates collapse/expand, updates DOM styles, and persists tocVisible in localStorage, restoring overflow after animation when expanding.
  • Expose toggleToc globally on window for potential host-side invocation.
QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Resources/md2html.html
Add a configurable keyboard shortcut handler that toggles the TOC before RTL/LTR shortcuts and respects documents without a TOC.
  • Add matchesShortcut(e, shortcut) to parse strings like "Ctrl+Shift+L" with optional Ctrl/Shift/Alt modifiers and case-insensitive key matching against KeyboardEvent.key.
  • Inject a TOC_TOGGLE_SHORTCUT template placeholder and use it in the keydown handler to toggle the TOC, preventing default when matched and returning early.
  • Ensure the TOC toggle no-ops when there are fewer than two headings, since the TOC is hidden in that case.
  • Update isInResizeArea to bail out when tocIsVisible is false, disabling resize interaction on a collapsed TOC.
  • Temporarily disable #toc CSS transition during drag-resize and restore it afterward so manual resizing remains responsive and still animated for programmatic toggles.
QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Resources/md2html.html
Wire the TOC toggle shortcut through Markdown plugin configuration and document the new option.
  • Read ToggleTocKey from QuickLook.Plugin.MarkdownViewer.config via SettingHelper.Get, defaulting to Ctrl+Shift+L, and inject it into the HTML as {{tocShortcut}}.
  • Extend the HTML template replacement chain to substitute {{tocShortcut}} alongside {{content}} and {{rtl}}.
  • Add OPTIONS.md documentation for the new option, including default value, type, description, and example values.
QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/MarkdownPanel.cs
OPTIONS.md

Assessment against linked issues

Issue Objective Addressed Explanation
#1934 Add a keyboard shortcut that toggles the Markdown viewer's Table of Contents (TOC) visibility.
#1934 Make the TOC toggle keyboard shortcut fully configurable via the plugin's configuration file (and not hard-coded).

Possibly linked issues

  • #[Feature Request] Add configurable keyboard shortcuts to toggle TOC visibility: PR implements a configurable keyboard shortcut (default Ctrl+Shift+L) to toggle TOC visibility as requested

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 1 issue, and left some high level feedback:

  • The tocShortcut value is injected directly into the JS string literal (const TOC_TOGGLE_SHORTCUT = "{{tocShortcut}}";) without escaping, so a config value containing quotes or backslashes could break the script or alter behavior; consider HTML/JS-escaping the value before template substitution or passing it via a data attribute instead.
  • The matchesShortcut implementation splits on "+" which makes it impossible to express keys whose KeyboardEvent.key is literally "+" (and similar edge cases), so you may want to either document this limitation or adjust the parsing to support such keys (e.g., using named tokens like Plus or a different separator).
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `tocShortcut` value is injected directly into the JS string literal (`const TOC_TOGGLE_SHORTCUT = "{{tocShortcut}}";`) without escaping, so a config value containing quotes or backslashes could break the script or alter behavior; consider HTML/JS-escaping the value before template substitution or passing it via a data attribute instead.
- The `matchesShortcut` implementation splits on `"+"` which makes it impossible to express keys whose `KeyboardEvent.key` is literally `"+"` (and similar edge cases), so you may want to either document this limitation or adjust the parsing to support such keys (e.g., using named tokens like `Plus` or a different separator).

## Individual Comments

### Comment 1
<location path="QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/MarkdownPanel.cs" line_range="97-100" />
<code_context>
                 isRtl = true;
         }

+        var tocShortcut = SettingHelper.Get("ToggleTocKey", "Ctrl+Shift+L", "QuickLook.Plugin.MarkdownViewer");
+
         var html = template.Replace("{{content}}", content)
-                           .Replace("{{rtl}}", isRtl ? "rtl" : "ltr");
+                           .Replace("{{rtl}}", isRtl ? "rtl" : "ltr")
+                           .Replace("{{tocShortcut}}", tocShortcut);

</code_context>
<issue_to_address>
**🚨 issue (security):** Safely serialize `tocShortcut` for insertion into the HTML/JS template.

`tocShortcut` is currently injected directly into a JS string literal in the template. If a user sets it to a value containing quotes or special characters, it can break the HTML/JS or allow script injection. Please ensure it’s properly escaped before `Replace("{{tocShortcut}}", ...)`, for example by serializing it with `JsonSerializer.Serialize(tocShortcut)` and then emitting `const TOC_TOGGLE_SHORTCUT = ${jsShortcut};` in the template.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +97 to +100
var tocShortcut = SettingHelper.Get("ToggleTocKey", "Ctrl+Shift+L", "QuickLook.Plugin.MarkdownViewer");

var html = template.Replace("{{content}}", content)
.Replace("{{rtl}}", isRtl ? "rtl" : "ltr");
.Replace("{{rtl}}", isRtl ? "rtl" : "ltr")
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🚨 issue (security): Safely serialize tocShortcut for insertion into the HTML/JS template.

tocShortcut is currently injected directly into a JS string literal in the template. If a user sets it to a value containing quotes or special characters, it can break the HTML/JS or allow script injection. Please ensure it’s properly escaped before Replace("{{tocShortcut}}", ...), for example by serializing it with JsonSerializer.Serialize(tocShortcut) and then emitting const TOC_TOGGLE_SHORTCUT = ${jsShortcut}; in the template.

@emako emako merged commit 75c9f19 into master May 9, 2026
6 checks passed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a configurable keyboard shortcut to toggle the Markdown viewer’s Table of Contents (TOC) panel, including a persisted collapsed/expanded state and a width transition animation, with documentation for the new plugin config option.

Changes:

  • Update md2html.html to support no-flicker initial TOC state, animated TOC collapse/expand, and a shortcut parser/handler.
  • Update MarkdownPanel.cs to read ToggleTocKey from QuickLook.Plugin.MarkdownViewer.config and inject it into the HTML template.
  • Document the new ToggleTocKey option in OPTIONS.md.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/Resources/md2html.html Adds TOC visibility persistence, slide animation, shortcut matching, and resize guarding when collapsed.
QuickLook.Plugin/QuickLook.Plugin.MarkdownViewer/MarkdownPanel.cs Reads ToggleTocKey from plugin config and injects it into the HTML template.
OPTIONS.md Documents the new MarkdownViewer config option ToggleTocKey.

Comment on lines +447 to 461
// Helper: check if a KeyboardEvent matches a shortcut string like "Ctrl+Shift+L"
// Supported modifiers: Ctrl, Shift, Alt. Key name is case-insensitive and matches e.key.
function matchesShortcut(e, shortcut) {
if (!shortcut) return false;
var parts = shortcut.toLowerCase().split("+").map(function (p) { return p.trim(); }).filter(Boolean);
if (parts.length === 0) return false;
var key = parts[parts.length - 1];
return e.key.toLowerCase() === key
&& e.ctrlKey === parts.includes("ctrl")
&& e.shiftKey === parts.includes("shift")
&& e.altKey === parts.includes("alt");
}

const TOC_TOGGLE_SHORTCUT = "{{tocShortcut}}";

Comment on lines +97 to +101
var tocShortcut = SettingHelper.Get("ToggleTocKey", "Ctrl+Shift+L", "QuickLook.Plugin.MarkdownViewer");

var html = template.Replace("{{content}}", content)
.Replace("{{rtl}}", isRtl ? "rtl" : "ltr");
.Replace("{{rtl}}", isRtl ? "rtl" : "ltr")
.Replace("{{tocShortcut}}", tocShortcut);
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.

[Feature Request] Add configurable keyboard shortcuts to toggle TOC visibility / 增加可配置的目录(TOC)显示隐藏快捷键

3 participants