fix(ui): add in-document search to the Markdown file preview

The rendered Markdown preview had no way to search: the Electron desktop
shell implements no find-in-page at all, and CodeMirror's search panel only
exists in edit mode, so Ctrl/Cmd+F in the preview was a dead shortcut (web
browsers happen to find plain-DOM text natively, but desktop does not).

Adds a compact find bar for the rendered preview (Ctrl/Cmd+F or the search
button): case-insensitive match highlighting with a live count, Enter /
Shift+Enter and arrow buttons to navigate matches, Esc to close. Matches
are wrapped in <mark> elements and re-applied via MutationObserver when the
markdown renderer re-morphs the container (theme/content changes); svg
(mermaid) and script/style text is skipped. The pure match-range logic is
unit-tested.

Fixes #2401
This commit is contained in:
Serhii Dziupin
2026-08-05 14:05:12 +03:00
parent 34c221b07f
commit 17d5b90d83
15 changed files with 512 additions and 22 deletions
+6
View File
@@ -1160,6 +1160,12 @@ export const dict = {
'filesView.editor.disableLineWrap': 'Zeilenumbruch deaktivieren',
'filesView.editor.enableLineWrap': 'Zeilenumbruch aktivieren',
'filesView.editor.findInFile': 'In Datei suchen',
'filesView.preview.find.placeholder': 'In Vorschau suchen',
'filesView.preview.find.nextAria': 'Nächster Treffer',
'filesView.preview.find.previousAria': 'Vorheriger Treffer',
'filesView.preview.find.closeAria': 'Suche schließen',
'filesView.preview.find.noMatches': 'Keine Treffer',
'filesView.preview.find.countAria': '{current} von {total}',
'filesView.editor.goToLine': 'Gehe zu Zeile',
'filesView.editor.switchToEditMode': 'Zum Bearbeitungsmodus wechseln',
'filesView.editor.switchToPreviewMode': 'Zum Vorschau-Modus wechseln',
+6
View File
@@ -1307,6 +1307,12 @@ export const dict = {
'filesView.editor.disableLineWrap': 'Disable line wrap',
'filesView.editor.enableLineWrap': 'Enable line wrap',
'filesView.editor.findInFile': 'Find in file',
'filesView.preview.find.placeholder': 'Find in preview',
'filesView.preview.find.nextAria': 'Next match',
'filesView.preview.find.previousAria': 'Previous match',
'filesView.preview.find.closeAria': 'Close search',
'filesView.preview.find.noMatches': 'No matches',
'filesView.preview.find.countAria': '{current} of {total}',
'filesView.editor.goToLine': 'Go to line',
'filesView.editor.switchToEditMode': 'Switch to edit mode',
'filesView.editor.switchToPreviewMode': 'Switch to preview mode',
+6
View File
@@ -1273,6 +1273,12 @@ export const dict: Record<I18nKey, string> = {
"filesView.editor.disableLineWrap": "Desactivar ajuste de línea",
"filesView.editor.enableLineWrap": "Activar ajuste de línea",
"filesView.editor.findInFile": "Buscar en el archivo",
"filesView.preview.find.placeholder": "Buscar en la vista previa",
"filesView.preview.find.nextAria": "Siguiente coincidencia",
"filesView.preview.find.previousAria": "Coincidencia anterior",
"filesView.preview.find.closeAria": "Cerrar búsqueda",
"filesView.preview.find.noMatches": "Sin coincidencias",
"filesView.preview.find.countAria": "{current} de {total}",
"filesView.editor.goToLine": "Ir a línea",
"filesView.editor.switchToEditMode": "Cambiar al modo de edición",
"filesView.editor.switchToPreviewMode": "Cambiar al modo de vista previa",
+6
View File
@@ -1129,6 +1129,12 @@ export const dict = {
'filesView.editor.disableLineWrap': 'Désactiver le retour à la ligne',
'filesView.editor.enableLineWrap': 'Activer le retour à la ligne',
'filesView.editor.findInFile': 'Rechercher dans le fichier',
'filesView.preview.find.placeholder': 'Rechercher dans l\'aperçu',
'filesView.preview.find.nextAria': 'Correspondance suivante',
'filesView.preview.find.previousAria': 'Correspondance précédente',
'filesView.preview.find.closeAria': 'Fermer la recherche',
'filesView.preview.find.noMatches': 'Aucune correspondance',
'filesView.preview.find.countAria': '{current} sur {total}',
'filesView.editor.goToLine': 'Aller à la ligne',
'filesView.editor.switchToEditMode': 'Passer en mode édition',
'filesView.editor.switchToPreviewMode': 'Passer en mode aperçu',
+6
View File
@@ -1303,6 +1303,12 @@ export const dict: Record<I18nKey, string> = {
'filesView.editor.disableLineWrap': '行の折り返しを無効にする',
'filesView.editor.enableLineWrap': '行の折り返しを有効にする',
'filesView.editor.findInFile': 'ファイル内を検索',
'filesView.preview.find.placeholder': 'プレビュー内を検索',
'filesView.preview.find.nextAria': '次の一致',
'filesView.preview.find.previousAria': '前の一致',
'filesView.preview.find.closeAria': '検索を閉じる',
'filesView.preview.find.noMatches': '一致なし',
'filesView.preview.find.countAria': '{total}件中{current}件目',
'filesView.editor.goToLine': '指定行に移動',
'filesView.editor.switchToEditMode': '編集モードに切り替え',
'filesView.editor.switchToPreviewMode': 'プレビューモードに切り替え',
+6
View File
@@ -1310,6 +1310,12 @@ export const dict: Record<I18nKey, string> = {
'filesView.editor.disableLineWrap': '줄 바꿈 끄기',
'filesView.editor.enableLineWrap': '줄 바꿈 켜기',
'filesView.editor.findInFile': '파일에서 찾기',
'filesView.preview.find.placeholder': '미리보기에서 찾기',
'filesView.preview.find.nextAria': '다음 일치 항목',
'filesView.preview.find.previousAria': '이전 일치 항목',
'filesView.preview.find.closeAria': '검색 닫기',
'filesView.preview.find.noMatches': '일치 항목 없음',
'filesView.preview.find.countAria': '{total}개 중 {current}번째',
'filesView.editor.goToLine': '줄로 이동',
'filesView.editor.switchToEditMode': '편집 모드로 전환',
'filesView.editor.switchToPreviewMode': '미리보기 모드로 전환',
+6
View File
@@ -1784,6 +1784,12 @@ export const dict: Record<I18nKey, string> = {
'filesView.editor.enableLineWrap': 'Włącz zawijanie linii',
'filesView.editor.exitFullscreen': 'Wyjdź z pełnego ekranu',
'filesView.editor.findInFile': 'Znajdź w pliku',
'filesView.preview.find.placeholder': 'Szukaj w podglądzie',
'filesView.preview.find.nextAria': 'Następne dopasowanie',
'filesView.preview.find.previousAria': 'Poprzednie dopasowanie',
'filesView.preview.find.closeAria': 'Zamknij wyszukiwanie',
'filesView.preview.find.noMatches': 'Brak dopasowań',
'filesView.preview.find.countAria': '{current} z {total}',
'filesView.editor.fullscreen': 'Pełny ekran',
'filesView.editor.goToLine': 'Przejdź do linii',
'filesView.editor.htmlPreviewTitle': 'Podgląd HTML',
@@ -1273,6 +1273,12 @@ export const dict: Record<I18nKey, string> = {
"filesView.editor.disableLineWrap": "Desativar ajuste de linha",
"filesView.editor.enableLineWrap": "Ativar ajuste de linha",
"filesView.editor.findInFile": "Buscar no arquivo",
"filesView.preview.find.placeholder": "Buscar na pré-visualização",
"filesView.preview.find.nextAria": "Próxima correspondência",
"filesView.preview.find.previousAria": "Correspondência anterior",
"filesView.preview.find.closeAria": "Fechar busca",
"filesView.preview.find.noMatches": "Sem correspondências",
"filesView.preview.find.countAria": "{current} de {total}",
"filesView.editor.goToLine": "Ir para linha",
"filesView.editor.switchToEditMode": "Alternar para o modo de edição",
"filesView.editor.switchToPreviewMode": "Alternar para o modo de visualização",
+6
View File
@@ -1273,6 +1273,12 @@ export const dict: Record<I18nKey, string> = {
"filesView.editor.disableLineWrap": "Вимкнути перенос рядків",
"filesView.editor.enableLineWrap": "Увімкнути перенос рядків",
"filesView.editor.findInFile": "Знайти у файлі",
"filesView.preview.find.placeholder": "Пошук у попередньому перегляді",
"filesView.preview.find.nextAria": "Наступний збіг",
"filesView.preview.find.previousAria": "Попередній збіг",
"filesView.preview.find.closeAria": "Закрити пошук",
"filesView.preview.find.noMatches": "Збігів немає",
"filesView.preview.find.countAria": "{current} із {total}",
"filesView.editor.goToLine": "Перейти до рядка",
"filesView.editor.switchToEditMode": "Перемкнутися в режим редагування",
"filesView.editor.switchToPreviewMode": "Перемкнутися в режим попереднього перегляду",
@@ -1273,6 +1273,12 @@ export const dict: Record<I18nKey, string> = {
'filesView.editor.disableLineWrap': '关闭自动换行',
'filesView.editor.enableLineWrap': '开启自动换行',
'filesView.editor.findInFile': '文件内查找',
'filesView.preview.find.placeholder': '在预览中查找',
'filesView.preview.find.nextAria': '下一个匹配',
'filesView.preview.find.previousAria': '上一个匹配',
'filesView.preview.find.closeAria': '关闭搜索',
'filesView.preview.find.noMatches': '无匹配项',
'filesView.preview.find.countAria': '第 {current} 个,共 {total} 个',
'filesView.editor.goToLine': '跳转到行',
'filesView.editor.switchToEditMode': '切换到编辑模式',
'filesView.editor.switchToPreviewMode': '切换到预览模式',
@@ -1284,6 +1284,12 @@ export const dict: Record<I18nKey, string> = {
'filesView.editor.disableLineWrap': '關閉自動換行',
'filesView.editor.enableLineWrap': '開啟自動換行',
'filesView.editor.findInFile': '檔案內尋找',
'filesView.preview.find.placeholder': '在預覽中尋找',
'filesView.preview.find.nextAria': '下一個相符項目',
'filesView.preview.find.previousAria': '上一個相符項目',
'filesView.preview.find.closeAria': '關閉搜尋',
'filesView.preview.find.noMatches': '無相符項目',
'filesView.preview.find.countAria': '第 {current} 個,共 {total} 個',
'filesView.editor.goToLine': '跳轉到行',
'filesView.editor.switchToEditMode': '切換到編輯模式',
'filesView.editor.switchToPreviewMode': '切換到預覽模式',