diff --git a/packages/ui/src/components/views/FilesView.tsx b/packages/ui/src/components/views/FilesView.tsx index 73219d05..1f8765dc 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'; @@ -968,6 +969,11 @@ 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 mdFullscreenPreviewContainerRef = React.useRef(null); const canCreateFile = Boolean(files.writeFile); const canCreateFolder = Boolean(files.createDirectory); @@ -2915,6 +2921,34 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { setIsGoToLineOpen(true); }); + // 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; + if (target instanceof Element && 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(() => { @@ -3366,6 +3400,23 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { /> )} + {isMarkdown && getMdViewMode() === 'preview' && ( + withTooltip(t('filesView.editor.findInFile'), + + ) + )} + {isMarkdown && getMdViewMode() === 'preview' && showMessageTTSButtons && ( @@ -3842,34 +3893,50 @@ 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')} -
-
- } +
+
{ + markdownPreviewRef.current = node; + mdPreviewContainerRef.current = node; + }} > - - + {fileContent.length > 500 * 1024 && ( +
+ {t('filesView.warning.largeFilePreviewLimited', { sizeKb: Math.round(fileContent.length / 1024) })} +
+ )} + +
{t('filesView.error.previewUnavailable')}
+
+ {t('filesView.error.switchToEditMode')} +
+
+ } + > + + +
+ {!isFullscreen && ( + + )} ) : selectedFile && isHtml && htmlViewMode === 'preview' ? ( isHtmlAssetAuthLoading ? ( @@ -4213,7 +4280,20 @@ export const FilesView: React.FC = ({ mode = 'full' }) => { ) : null} ) : isMarkdown && getMdViewMode() === 'preview' ? ( -
+
{ + markdownPreviewRef.current = node; + mdFullscreenPreviewContainerRef.current = node; + }} + > + {selectedFile ? ( { + 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..7113b62b --- /dev/null +++ b/packages/ui/src/components/views/MarkdownPreviewSearch.tsx @@ -0,0 +1,344 @@ +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 { cn } from '@/lib/utils'; +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-status-warning/30 text-foreground'; +const CURRENT_MARK_CLASS = 'rounded-[2px] bg-status-warning/60 text-foreground'; +/** Keystrokes re-walk the whole preview, so coalesce bursts of typing. */ +const SEARCH_DEBOUNCE_MS = 120; + +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()) { + const node = walker.currentNode; + if (node instanceof Text) { + textNodes.push(node); + } + } + + 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; + /** Layout overrides for the floating bar (position, offsets). */ + className?: string; +}; + +export const MarkdownPreviewSearch: React.FC = ({ + containerRef, + open, + onOpenChange, + focusNonce, + className, +}) => { + 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 debounceRef = React.useRef | null>(null); + // Focus returns here when the bar closes, so Escape does not strand focus. + const returnFocusRef = React.useRef(null); + + 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]); + + const scheduleSearch = React.useCallback((nextQuery: string) => { + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + debounceRef.current = setTimeout(() => { + debounceRef.current = null; + runSearch(nextQuery); + }, SEARCH_DEBOUNCE_MS); + }, [runSearch]); + + React.useEffect(() => () => { + if (debounceRef.current) { + clearTimeout(debounceRef.current); + } + }, []); + + const close = React.useCallback(() => { + onOpenChange(false); + const target = returnFocusRef.current; + returnFocusRef.current = null; + if (target?.isConnected) { + target.focus(); + } + }, [onOpenChange]); + + // 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, remembering what to restore on close. + React.useEffect(() => { + if (!open) { + return; + } + const previous = document.activeElement; + if (previous instanceof HTMLElement && !returnFocusRef.current) { + returnFocusRef.current = previous; + } + 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(); + close(); + } + }, [close, goToNext, goToPrevious]); + + if (!open) { + return null; + } + + return ( +
+ + { + setQuery(event.target.value); + scheduleSearch(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 6da29800..064ccf46 100644 --- a/packages/ui/src/lib/i18n/messages/de.ts +++ b/packages/ui/src/lib/i18n/messages/de.ts @@ -1244,6 +1244,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 4bf7cc24..d3ccb5a5 100644 --- a/packages/ui/src/lib/i18n/messages/en.ts +++ b/packages/ui/src/lib/i18n/messages/en.ts @@ -1439,6 +1439,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 30cbbe22..d60320e0 100644 --- a/packages/ui/src/lib/i18n/messages/es.ts +++ b/packages/ui/src/lib/i18n/messages/es.ts @@ -1405,6 +1405,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 4ad68386..5667df24 100644 --- a/packages/ui/src/lib/i18n/messages/fr.ts +++ b/packages/ui/src/lib/i18n/messages/fr.ts @@ -1205,6 +1205,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 3f58af13..49ef26c0 100644 --- a/packages/ui/src/lib/i18n/messages/ja.ts +++ b/packages/ui/src/lib/i18n/messages/ja.ts @@ -1435,6 +1435,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 f09dede4..88bafdde 100644 --- a/packages/ui/src/lib/i18n/messages/ko.ts +++ b/packages/ui/src/lib/i18n/messages/ko.ts @@ -1441,6 +1441,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 cb2003d7..c5e4da2f 100644 --- a/packages/ui/src/lib/i18n/messages/pl.ts +++ b/packages/ui/src/lib/i18n/messages/pl.ts @@ -1953,6 +1953,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 a4c91f42..9eb29f01 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.ts @@ -1405,6 +1405,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/tr.ts b/packages/ui/src/lib/i18n/messages/tr.ts index 2585b96f..bb1955b4 100644 --- a/packages/ui/src/lib/i18n/messages/tr.ts +++ b/packages/ui/src/lib/i18n/messages/tr.ts @@ -1414,6 +1414,12 @@ export const dict = { 'filesView.editor.disableLineWrap': 'Satır kaydırmayı devre dışı bırak', 'filesView.editor.enableLineWrap': 'Satır kaydırmayı etkinleştir', 'filesView.editor.findInFile': 'Dosyada bul', + 'filesView.preview.find.placeholder': 'Önizlemede bul', + 'filesView.preview.find.nextAria': 'Sonraki eşleşme', + 'filesView.preview.find.previousAria': 'Önceki eşleşme', + 'filesView.preview.find.closeAria': 'Aramayı kapat', + 'filesView.preview.find.noMatches': 'Eşleşme yok', + 'filesView.preview.find.countAria': '{total} içinde {current}', 'filesView.editor.goToLine': 'Satıra git', 'filesView.editor.switchToEditMode': 'Düzenleme moduna geç', 'filesView.editor.switchToPreviewMode': 'Önizleme moduna geç', diff --git a/packages/ui/src/lib/i18n/messages/uk.ts b/packages/ui/src/lib/i18n/messages/uk.ts index 35bd94a5..69b8b662 100644 --- a/packages/ui/src/lib/i18n/messages/uk.ts +++ b/packages/ui/src/lib/i18n/messages/uk.ts @@ -1405,6 +1405,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 c15eed5e..c0953b8c 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.ts @@ -1405,6 +1405,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 3f440ad1..d1e49a8f 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.ts @@ -1416,6 +1416,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': '切換到預覽模式',