diff --git a/package-lock.json b/package-lock.json index 35769e4..1558733 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "markpad", - "version": "2.6.6", + "version": "2.6.8", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "markpad", - "version": "2.6.6", + "version": "2.6.8", "license": "MIT", "dependencies": { "@tauri-apps/api": "^2", diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 846f0e5..572eec1 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1028,6 +1028,12 @@ pub fn run() { .item(&PredefinedMenuItem::copy(app, None)?) .item(&PredefinedMenuItem::paste(app, None)?) .item(&PredefinedMenuItem::select_all(app, None)?) + .separator() + .item( + &MenuItemBuilder::with_id("menu-edit-find", "Find…") + .accelerator("CmdOrCtrl+F") + .build(app)?, + ) .build()?; let window_submenu = SubmenuBuilder::new(app, "Window") @@ -1139,7 +1145,10 @@ pub fn run() { if id == "check-updates" { let _ = window.emit("menu-check-updates", ()); - } else if id == "menu-app-quit" || id.starts_with("menu-file-") { + } else if id == "menu-app-quit" + || id.starts_with("menu-file-") + || id.starts_with("menu-edit-") + { let _ = window.emit(id, ()); } }) diff --git a/src/lib/MarkdownViewer.svelte b/src/lib/MarkdownViewer.svelte index 7191728..d4e3de2 100644 --- a/src/lib/MarkdownViewer.svelte +++ b/src/lib/MarkdownViewer.svelte @@ -18,6 +18,7 @@ import ContextMenu, { type ContextMenuItem } from './components/ContextMenu.svelte'; import Toc from './components/Toc.svelte'; import Toast from './components/Toast.svelte'; + import FindBar from './components/FindBar.svelte'; import { exportAsHtml as _exportHtml, exportAsPdf } from './utils/export'; import ZoomOverlay from './components/ZoomOverlay.svelte'; import { processMarkdownHtml } from './utils/markdown'; @@ -76,9 +77,28 @@ import { t } from './utils/i18n.js'; undo: () => void; redo: () => void; revealHeader: (text: string) => void; + triggerFind: () => void; } | null>(null); let liveMode = $state(false); + let findOpen = $state(false); + let findBar = $state<{ reapply: () => void; clearHighlights: () => void } | null>(null); + + // Decide where Cmd/Ctrl+F should land based on what's visible and where + // focus is. Used by both the JS keydown handler (Win/Linux + macOS in-page + // shortcut) and the macOS native menu listener (which fires Cmd+F via the + // Edit menu accelerator and bypasses the JS keydown path). + function triggerFindAction() { + const active = document.activeElement as Node | null; + const editorHasFocus = !!editorPaneEl && !!active && editorPaneEl.contains(active); + const previewVisible = !isEditing || !!tabManager.activeTab?.isSplit; + if (editorHasFocus || !previewVisible) { + editorPane?.triggerFind?.(); + } else if (markdownBody) { + findOpen = true; + } + } + let isDragging = $state(false); let dragTarget = $state<'editor' | 'preview' | null>(null); let editorPaneEl = $state(); @@ -331,6 +351,7 @@ import { t } from './utils/i18n.js'; $effect(() => { const _ = tabManager.activeTabId; showHome = false; + findOpen = false; }); function processHighlights(root: Element) { @@ -693,6 +714,16 @@ import { t } from './utils/i18n.js'; if (sanitizedHtml && markdownBody && !isEditing && hljs && renderMathInElement && mermaid) renderRichContent(); }); + // Re-apply find highlights after the preview HTML is replaced. The + // `bind:innerHTML={sanitizedHtml}` on the article wipes the DOM on every + // edit/render pass; without this, highlights vanish until the user + // re-types in the find bar. + $effect(() => { + const _ = sanitizedHtml; + if (!findOpen || !findBar) return; + tick().then(() => findBar?.reapply()); + }); + $effect(() => { // Depend on the ID and body existence to trigger restore const id = tabManager.activeTabId; @@ -2069,6 +2100,18 @@ import { t } from './utils/i18n.js'; e.preventDefault(); showSettings = !showSettings; } + // Ctrl/Cmd+F: route to either Monaco's built-in find or the preview + // FindBar depending on focus and which panes are visible. We only + // preventDefault when we actually take the action ourselves — + // otherwise we let Monaco's own keybinding fire. + if (cmdOrCtrl && !e.shiftKey && !e.altKey && key === 'f') { + const active = document.activeElement as Node | null; + const editorHasFocus = !!editorPaneEl && !!active && editorPaneEl.contains(active); + if (!editorHasFocus) { + e.preventDefault(); + triggerFindAction(); + } + } } function pushScrollHistory() { @@ -2283,6 +2326,11 @@ import { t } from './utils/i18n.js'; toggleEdit(); }), ); + unlisteners.push( + await listen('menu-edit-find', () => { + triggerFindAction(); + }), + ); unlisteners.push( await listen('menu-tab-rename', async (event) => { const tabId = event.payload as string; @@ -2577,6 +2625,7 @@ import { t } from './utils/i18n.js'; {theme} onSetTheme={(t) => (theme = t)} onopenSettings={() => (showSettings = true)} + onfind={triggerFindAction} oncloseTab={closeTabAndWindowIfLast} />
@@ -2620,6 +2669,7 @@ import { t } from './utils/i18n.js'; {theme} onSetTheme={(t) => (theme = t)} onopenSettings={() => (showSettings = true)} + onfind={triggerFindAction} oncloseTab={closeTabAndWindowIfLast} /> (theme = t)} onclose={() => (showSettings = false)} /> @@ -2675,7 +2725,13 @@ import { t } from './utils/i18n.js'; class="pane viewer-pane" class:active={!isEditing || isSplit} style="flex: {isSplit ? 1 - tabManager.activeTab.splitRatio : (!isEditing) ? 1 : 0}"> - + + +
{ + if (!editor) return; + editor.focus(); + editor.getAction("actions.find")?.run(); + } + export const getValue = () => editor?.getValue() || ""; export const setValue = (val: string) => editor?.setValue(val); export const focus = () => editor?.focus(); diff --git a/src/lib/components/FindBar.svelte b/src/lib/components/FindBar.svelte new file mode 100644 index 0000000..fc0c7ca --- /dev/null +++ b/src/lib/components/FindBar.svelte @@ -0,0 +1,519 @@ + + +{#if open} + + +{/if} + + diff --git a/src/lib/components/TitleBar.svelte b/src/lib/components/TitleBar.svelte index 0ef0663..b873d5d 100644 --- a/src/lib/components/TitleBar.svelte +++ b/src/lib/components/TitleBar.svelte @@ -51,6 +51,7 @@ theme = 'system', onSetTheme, onopenSettings, + onfind, } = $props<{ isFocused: boolean; isScrolled: boolean; @@ -88,6 +89,7 @@ theme?: string; onSetTheme?: (theme: string) => void; onopenSettings?: () => void; + onfind?: () => void; }>(); const appWindow = getCurrentWindow(); @@ -206,6 +208,12 @@ if (isMarkdown && !tabManager.activeTab?.isSplit) { list.push('edit'); } + // Find in preview: only meaningful when a preview is actually + // visible (view mode or split). In pure edit mode Monaco's own + // Ctrl+F handles search, so we hide the entry there. + if (isMarkdown && (!isEditing || tabManager.activeTab?.isSplit)) { + list.push('find'); + } list.push('zen'); list.push('tabs'); } @@ -548,6 +556,26 @@ {t('menu.tabs', currentLanguage).replace('{{action}}', settings.showTabs ? t('menu.hide', currentLanguage) : t('menu.show', currentLanguage) )} {modifier}+Shift+B + {:else if id === 'find'} + {:else if id === 'open_loc'}