From 17d5b90d834493c9d355d5ba7b5b5d18781f50b7 Mon Sep 17 00:00:00 2001 From: Serhii Dziupin Date: Wed, 5 Aug 2026 14:05:12 +0300 Subject: [PATCH] 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 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 --- .../ui/src/components/views/FilesView.tsx | 102 ++++-- .../views/MarkdownPreviewSearch.test.ts | 41 +++ .../views/MarkdownPreviewSearch.tsx | 302 ++++++++++++++++++ .../components/views/markdownPreviewFind.ts | 23 ++ packages/ui/src/lib/i18n/messages/de.ts | 6 + packages/ui/src/lib/i18n/messages/en.ts | 6 + packages/ui/src/lib/i18n/messages/es.ts | 6 + packages/ui/src/lib/i18n/messages/fr.ts | 6 + packages/ui/src/lib/i18n/messages/ja.ts | 6 + packages/ui/src/lib/i18n/messages/ko.ts | 6 + packages/ui/src/lib/i18n/messages/pl.ts | 6 + packages/ui/src/lib/i18n/messages/pt-BR.ts | 6 + packages/ui/src/lib/i18n/messages/uk.ts | 6 + packages/ui/src/lib/i18n/messages/zh-CN.ts | 6 + packages/ui/src/lib/i18n/messages/zh-TW.ts | 6 + 15 files changed, 512 insertions(+), 22 deletions(-) create mode 100644 packages/ui/src/components/views/MarkdownPreviewSearch.test.ts create mode 100644 packages/ui/src/components/views/MarkdownPreviewSearch.tsx create mode 100644 packages/ui/src/components/views/markdownPreviewFind.ts diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 951e9dd2..74887230 100644 --- a/packages/ui/src/components/views/FilesView.tsx +++ b/packages/ui/src/components/views/FilesView.tsx @@ -24,6 +24,7 @@ import { Button } from '@/components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'; import { CodeMirrorEditor } from '@/components/ui/CodeMirrorEditor'; import { GoToLineDialog } from './GoToLineDialog'; +import { MarkdownPreviewSearch } from './MarkdownPreviewSearch'; import { PreviewToggleButton } from './PreviewToggleButton'; import { JsonTreeView } from '@/components/ui/JsonTreeView'; import { SimpleMarkdownRenderer } from '@/components/chat/MarkdownRenderer'; @@ -956,6 +957,10 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { const [copiedContent, setCopiedContent] = React.useState(false); const [copiedPath, setCopiedPath] = React.useState(false); const [isGoToLineOpen, setIsGoToLineOpen] = React.useState(false); + // In-preview find for the rendered Markdown preview (Ctrl/Cmd+F). + const [mdPreviewFindOpen, setMdPreviewFindOpen] = React.useState(false); + const [mdPreviewFindFocusNonce, setMdPreviewFindFocusNonce] = React.useState(0); + const mdPreviewContainerRef = React.useRef(null); const canCreateFile = Boolean(files.writeFile); const canCreateFolder = Boolean(files.createDirectory); @@ -2945,6 +2950,34 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { return () => window.removeEventListener('keydown', handleKeyDown); }, [canEdit, isMobile, shortcutOverrides, textViewMode]); + // Ctrl/Cmd+F opens the in-preview find bar for the rendered Markdown + // preview. In edit mode CodeMirror owns the shortcut, so this handler is + // active only while the preview is shown. + React.useEffect(() => { + if (!isMarkdown || getMdViewMode() !== 'preview') { + return; + } + + const handleKeyDown = (event: KeyboardEvent) => { + if (!(event.metaKey || event.ctrlKey) || event.shiftKey || event.altKey) { + return; + } + if (event.key.toLowerCase() !== 'f') { + return; + } + const target = event.target as Element | null; + if (target?.closest('[role="dialog"]')) { + return; + } + event.preventDefault(); + setMdPreviewFindOpen(true); + setMdPreviewFindFocusNonce((value) => value + 1); + }; + + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [getMdViewMode, isMarkdown]); + const editorFontSize = useUIStore((state) => state.editorFontSize); const editorExtensions = React.useMemo(() => { @@ -3392,6 +3425,23 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { /> )} + {isMarkdown && getMdViewMode() === 'preview' && ( + withTooltip(t('filesView.editor.findInFile'), + + ) + )} + {isMarkdown && getMdViewMode() === 'preview' && showMessageTTSButtons && ( @@ -3932,29 +3982,37 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { ) : selectedFile && isMarkdown && getMdViewMode() === 'preview' ? ( -
- {fileContent.length > 500 * 1024 && ( -
- {t('filesView.warning.largeFilePreviewLimited', { sizeKb: Math.round(fileContent.length / 1024) })} -
- )} - -
{t('filesView.error.previewUnavailable')}
-
- {t('filesView.error.switchToEditMode')} -
+
+
+ {fileContent.length > 500 * 1024 && ( +
+ {t('filesView.warning.largeFilePreviewLimited', { sizeKb: Math.round(fileContent.length / 1024) })}
- } - > - - + )} + +
{t('filesView.error.previewUnavailable')}
+
+ {t('filesView.error.switchToEditMode')} +
+
+ } + > + + +
+
) : selectedFile && isHtml && htmlViewMode === 'preview' ? ( isHtmlAssetAuthLoading ? ( diff --git a/packages/ui/src/components/views/MarkdownPreviewSearch.test.ts b/packages/ui/src/components/views/MarkdownPreviewSearch.test.ts new file mode 100644 index 00000000..11c0e3a4 --- /dev/null +++ b/packages/ui/src/components/views/MarkdownPreviewSearch.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, test } from 'bun:test'; + +import { findMatchRanges } from './markdownPreviewFind'; + +describe('findMatchRanges', () => { + test('returns no ranges for an empty or whitespace-only query', () => { + expect(findMatchRanges('hello world', '')).toEqual([]); + expect(findMatchRanges('hello world', ' ')).toEqual([]); + }); + + test('returns no ranges when the query does not occur', () => { + expect(findMatchRanges('hello world', 'nope')).toEqual([]); + }); + + test('finds all non-overlapping occurrences', () => { + expect(findMatchRanges('the quick brown fox jumps over the lazy dog', 'the')).toEqual([ + { start: 0, end: 3 }, + { start: 31, end: 34 }, + ]); + }); + + test('matches case-insensitively', () => { + expect(findMatchRanges('Hello HELLO hello', 'hello')).toEqual([ + { start: 0, end: 5 }, + { start: 6, end: 11 }, + { start: 12, end: 17 }, + ]); + }); + + test('scans non-overlapping matches like standard find-in-page', () => { + expect(findMatchRanges('aaaa', 'aaa')).toEqual([{ start: 0, end: 3 }]); + }); + + test('trims the query before matching', () => { + expect(findMatchRanges('alpha beta', ' beta ')).toEqual([{ start: 6, end: 10 }]); + }); + + test('handles a query longer than the text', () => { + expect(findMatchRanges('abc', 'abcdef')).toEqual([]); + }); +}); diff --git a/packages/ui/src/components/views/MarkdownPreviewSearch.tsx b/packages/ui/src/components/views/MarkdownPreviewSearch.tsx new file mode 100644 index 00000000..c8e4bf48 --- /dev/null +++ b/packages/ui/src/components/views/MarkdownPreviewSearch.tsx @@ -0,0 +1,302 @@ +import React from 'react'; + +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Icon } from '@/components/icon/Icon'; +import { useI18n } from '@/lib/i18n'; +import { findMatchRanges } from './markdownPreviewFind'; + +/** + * In-preview text search for the rendered Markdown file preview. + * + * The preview renders as plain DOM (no iframe/shadow root), so browser-native + * find works on web — but the Electron desktop shell has no find-in-page + * implementation at all, and CodeMirror's search only exists in edit mode. + * This widget provides the find shortcut behavior (Ctrl/Cmd+F) and a compact + * search bar with match highlighting, navigation, and a live count, scoped to + * the preview container. + * + * The rendered DOM is owned by the markdown renderer (block-level morphdom + * reconciliation), so highlights are re-applied whenever the renderer mutates + * the container (theme or content changes) via a MutationObserver; mutations + * produced by this widget itself are ignored. + */ +const MARK_ATTR = 'data-md-find'; +const CURRENT_MARK_ATTR = 'data-md-find-current'; +const MARK_CLASS = 'rounded-[2px] bg-[var(--status-warning)]/40'; +const CURRENT_MARK_CLASS = 'rounded-[2px] bg-[var(--status-warning)]/80'; + +const isMarkElement = (node: Node): boolean => { + return node instanceof Element && node.hasAttribute(MARK_ATTR); +}; + +const clearHighlights = (container: HTMLElement): void => { + container.querySelectorAll(`mark[${MARK_ATTR}]`).forEach((mark) => { + const parent = mark.parentNode; + if (!parent) { + return; + } + parent.replaceChild(document.createTextNode(mark.textContent ?? ''), mark); + parent.normalize(); + }); +}; + +const applySearch = (container: HTMLElement, query: string): HTMLElement[] => { + clearHighlights(container); + + const normalized = query.trim().toLowerCase(); + if (!normalized) { + return []; + } + + const marks: HTMLElement[] = []; + const walker = document.createTreeWalker(container, NodeFilter.SHOW_TEXT, { + acceptNode(node) { + const parent = node.parentElement; + if (!parent) { + return NodeFilter.FILTER_REJECT; + } + // Skipping svg (mermaid) keeps the highlight pass from corrupting + // diagram rendering; script/style content is never visible anyway. + if (parent.closest('svg, script, style')) { + return NodeFilter.FILTER_REJECT; + } + return NodeFilter.FILTER_ACCEPT; + }, + }); + + const textNodes: Text[] = []; + while (walker.nextNode()) { + textNodes.push(walker.currentNode as Text); + } + + for (const node of textNodes) { + const text = node.nodeValue ?? ''; + if (!text) { + continue; + } + const ranges = findMatchRanges(text, normalized); + if (ranges.length === 0) { + continue; + } + + const parent = node.parentNode; + if (!parent) { + continue; + } + const fragment = document.createDocumentFragment(); + let cursor = 0; + for (const range of ranges) { + if (range.start > cursor) { + fragment.appendChild(document.createTextNode(text.slice(cursor, range.start))); + } + const mark = document.createElement('mark'); + mark.setAttribute(MARK_ATTR, ''); + mark.className = MARK_CLASS; + mark.textContent = text.slice(range.start, range.end); + fragment.appendChild(mark); + marks.push(mark); + cursor = range.end; + } + if (cursor < text.length) { + fragment.appendChild(document.createTextNode(text.slice(cursor))); + } + parent.replaceChild(fragment, node); + } + + return marks; +}; + +type MarkdownPreviewSearchProps = { + /** The scrollable preview container whose rendered text is searched. */ + containerRef: React.RefObject; + open: boolean; + onOpenChange: (open: boolean) => void; + /** Bumped every time the find shortcut is pressed to re-focus the input. */ + focusNonce: number; +}; + +export const MarkdownPreviewSearch: React.FC = ({ + containerRef, + open, + onOpenChange, + focusNonce, +}) => { + const { t } = useI18n(); + const [query, setQuery] = React.useState(''); + const [total, setTotal] = React.useState(0); + const [index, setIndex] = React.useState(0); + const inputRef = React.useRef(null); + const marksRef = React.useRef([]); + const queryRef = React.useRef(query); + queryRef.current = query; + + const runSearch = React.useCallback((nextQuery: string) => { + const container = containerRef.current; + if (!container) { + marksRef.current = []; + setTotal(0); + setIndex(0); + return; + } + marksRef.current = applySearch(container, nextQuery); + setTotal(marksRef.current.length); + setIndex(0); + }, [containerRef]); + + // Re-apply highlights when the renderer re-morphs the container (theme or + // content changes), ignoring mutations this widget produces itself. Only + // active while the bar is open; closing clears the highlights. + React.useEffect(() => { + const container = containerRef.current; + if (!open || !container) { + return; + } + const observer = new MutationObserver((records) => { + const fromUs = records.some((record) => { + if (record.target instanceof Element && record.target.hasAttribute(MARK_ATTR)) { + return true; + } + return [...record.addedNodes].some((node) => isMarkElement(node)); + }); + if (fromUs) { + return; + } + runSearch(queryRef.current); + }); + observer.observe(container, { childList: true, subtree: true, characterData: true }); + return () => { + observer.disconnect(); + clearHighlights(container); + }; + }, [containerRef, open, runSearch]); + + // Focus the input when the bar opens. + React.useEffect(() => { + if (open) { + inputRef.current?.focus(); + } + }, [open]); + + // Pressing the find shortcut again re-focuses and re-selects the query. + React.useEffect(() => { + if (open && focusNonce > 0) { + inputRef.current?.focus(); + inputRef.current?.select(); + } + }, [open, focusNonce]); + + // Keep the current-match highlight and scroll it into view. + React.useEffect(() => { + const container = containerRef.current; + if (!container) { + return; + } + container.querySelectorAll(`mark[${CURRENT_MARK_ATTR}]`).forEach((mark) => { + mark.removeAttribute(CURRENT_MARK_ATTR); + mark.className = MARK_CLASS; + }); + if (total === 0) { + return; + } + const current = marksRef.current[Math.min(Math.max(index, 0), total - 1)]; + if (!current) { + return; + } + current.setAttribute(CURRENT_MARK_ATTR, ''); + current.className = CURRENT_MARK_CLASS; + current.scrollIntoView({ block: 'nearest' }); + }, [containerRef, index, total]); + + const goToNext = React.useCallback(() => { + setIndex((current) => (total === 0 ? 0 : (current + 1) % total)); + }, [total]); + + const goToPrevious = React.useCallback(() => { + setIndex((current) => (total === 0 ? 0 : (current - 1 + total) % total)); + }, [total]); + + const handleKeyDown = React.useCallback((event: React.KeyboardEvent) => { + if (event.key === 'Enter') { + event.preventDefault(); + if (event.shiftKey) { + goToPrevious(); + } else { + goToNext(); + } + } else if (event.key === 'Escape') { + event.preventDefault(); + onOpenChange(false); + } + }, [goToNext, goToPrevious, onOpenChange]); + + if (!open) { + return null; + } + + return ( +
+ + { + setQuery(event.target.value); + runSearch(event.target.value); + }} + onKeyDown={handleKeyDown} + placeholder={t('filesView.preview.find.placeholder')} + aria-label={t('filesView.preview.find.placeholder')} + className="h-7 w-40 rounded-md px-2 py-0 text-sm md:w-56" + /> + 0 + ? t('filesView.preview.find.countAria', { current: index + 1, total }) + : undefined} + > + {query.trim() && total === 0 + ? t('filesView.preview.find.noMatches') + : total > 0 + ? `${index + 1}/${total}` + : ''} + + + + +
+ ); +}; diff --git a/packages/ui/src/components/views/markdownPreviewFind.ts b/packages/ui/src/components/views/markdownPreviewFind.ts new file mode 100644 index 00000000..0e876a08 --- /dev/null +++ b/packages/ui/src/components/views/markdownPreviewFind.ts @@ -0,0 +1,23 @@ +/** + * Case-insensitive substring match ranges over a single text string, using + * the same non-overlapping `String.prototype.indexOf` scan semantics as + * standard find-in-page (e.g. "aaa" in "aaaa" yields a single [0,3]). + */ +export const findMatchRanges = (text: string, query: string): Array<{ start: number; end: number }> => { + const normalized = query.trim().toLowerCase(); + const ranges: Array<{ start: number; end: number }> = []; + if (!normalized) { + return ranges; + } + const lower = text.toLowerCase(); + let cursor = 0; + while (true) { + const index = lower.indexOf(normalized, cursor); + if (index === -1) { + break; + } + ranges.push({ start: index, end: index + normalized.length }); + cursor = index + normalized.length; + } + return ranges; +}; diff --git a/packages/ui/src/lib/i18n/messages/de.ts b/packages/ui/src/lib/i18n/messages/de.ts index 57a22de2..ba2dfa50 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -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', diff --git a/packages/ui/src/lib/i18n/messages/en.ts b/packages/ui/src/lib/i18n/messages/en.ts index c77d6a07..50089591 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -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', diff --git a/packages/ui/src/lib/i18n/messages/es.ts b/packages/ui/src/lib/i18n/messages/es.ts index 72985914..113d5c00 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -1273,6 +1273,12 @@ export const dict: Record = { "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", diff --git a/packages/ui/src/lib/i18n/messages/fr.ts b/packages/ui/src/lib/i18n/messages/fr.ts index 986a4cd4..b5e1b549 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -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', diff --git a/packages/ui/src/lib/i18n/messages/ja.ts b/packages/ui/src/lib/i18n/messages/ja.ts index 0f4da4e7..813094d1 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -1303,6 +1303,12 @@ export const dict: Record = { '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': 'プレビューモードに切り替え', diff --git a/packages/ui/src/lib/i18n/messages/ko.ts b/packages/ui/src/lib/i18n/messages/ko.ts index 0f09fba2..c7af0a4a 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -1310,6 +1310,12 @@ export const dict: Record = { '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': '미리보기 모드로 전환', diff --git a/packages/ui/src/lib/i18n/messages/pl.ts b/packages/ui/src/lib/i18n/messages/pl.ts index 12f727b1..d3f78b5b 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -1784,6 +1784,12 @@ export const dict: Record = { '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', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.ts b/packages/ui/src/lib/i18n/messages/pt-BR.ts index 10dd7709..f65b4b43 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -1273,6 +1273,12 @@ export const dict: Record = { "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", diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index dd52bcb9..f2ee6c1b 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -1273,6 +1273,12 @@ export const dict: Record = { "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": "Перемкнутися в режим попереднього перегляду", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.ts b/packages/ui/src/lib/i18n/messages/zh-CN.ts index 35de2c9c..da730d2f 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -1273,6 +1273,12 @@ export const dict: Record = { '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': '切换到预览模式', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.ts b/packages/ui/src/lib/i18n/messages/zh-TW.ts index 6641606b..27c8d00c 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -1284,6 +1284,12 @@ export const dict: Record = { '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': '切換到預覽模式',