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

fix(ui): add in-document search to the Markdown file preview
This commit is contained in:
Bohdan Triapitsyn
2026-08-28 23:48:56 +03:00
committed by GitHub
16 changed files with 587 additions and 27 deletions
+107 -27
View File
@@ -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<FilesViewProps> = ({ 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<HTMLDivElement | null>(null);
const mdFullscreenPreviewContainerRef = React.useRef<HTMLDivElement | null>(null);
const canCreateFile = Boolean(files.writeFile);
const canCreateFolder = Boolean(files.createDirectory);
@@ -2915,6 +2921,34 @@ export const FilesView: React.FC<FilesViewProps> = ({ 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<FilesViewProps> = ({ mode = 'full' }) => {
/>
)}
{isMarkdown && getMdViewMode() === 'preview' && (
withTooltip(t('filesView.editor.findInFile'),
<Button
variant="ghost"
size="sm"
onClick={() => {
setMdPreviewFindOpen(true);
setMdPreviewFindFocusNonce((value) => value + 1);
}}
className="size-6 p-0 text-foreground opacity-100 transition-opacity hover:bg-transparent focus-visible:bg-transparent active:bg-transparent"
title={t('filesView.editor.findInFile')}
>
<Icon name="search" className="size-4" />
</Button>
)
)}
{isMarkdown && getMdViewMode() === 'preview' && showMessageTTSButtons && (
<Tooltip>
<TooltipTrigger asChild>
@@ -3842,34 +3893,50 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
</div>
</ErrorBoundary>
) : selectedFile && isMarkdown && getMdViewMode() === 'preview' ? (
<div className="oc-file-preview h-full overflow-auto p-3" ref={markdownPreviewRef}>
<FilePreviewCommentMenu
containerRef={markdownPreviewRef}
filePath={selectedFile.path}
fileContent={fileContent}
/>
{fileContent.length > 500 * 1024 && (
<div className="mb-3 rounded-md border border-status-warning/20 bg-status-warning/10 px-3 py-2 text-sm text-status-warning">
{t('filesView.warning.largeFilePreviewLimited', { sizeKb: Math.round(fileContent.length / 1024) })}
</div>
)}
<ErrorBoundary
fallback={
<div className="rounded-md border border-destructive/20 bg-destructive/10 px-3 py-2">
<div className="mb-1 font-medium text-destructive">{t('filesView.error.previewUnavailable')}</div>
<div className="text-sm text-muted-foreground">
{t('filesView.error.switchToEditMode')}
</div>
</div>
}
<div className="relative h-full min-h-0">
<div
className="oc-file-preview h-full overflow-auto p-3"
ref={(node) => {
markdownPreviewRef.current = node;
mdPreviewContainerRef.current = node;
}}
>
<SimpleMarkdownRenderer
content={fileContent}
className="typography-markdown-body"
stripFrontmatter
enableFileReferences={false}
<FilePreviewCommentMenu
containerRef={markdownPreviewRef}
filePath={selectedFile.path}
fileContent={fileContent}
/>
</ErrorBoundary>
{fileContent.length > 500 * 1024 && (
<div className="mb-3 rounded-md border border-status-warning/20 bg-status-warning/10 px-3 py-2 text-sm text-status-warning">
{t('filesView.warning.largeFilePreviewLimited', { sizeKb: Math.round(fileContent.length / 1024) })}
</div>
)}
<ErrorBoundary
fallback={
<div className="rounded-md border border-destructive/20 bg-destructive/10 px-3 py-2">
<div className="mb-1 font-medium text-destructive">{t('filesView.error.previewUnavailable')}</div>
<div className="text-sm text-muted-foreground">
{t('filesView.error.switchToEditMode')}
</div>
</div>
}
>
<SimpleMarkdownRenderer
content={fileContent}
className="typography-markdown-body"
stripFrontmatter
enableFileReferences={false}
/>
</ErrorBoundary>
</div>
{!isFullscreen && (
<MarkdownPreviewSearch
containerRef={mdPreviewContainerRef}
open={mdPreviewFindOpen}
onOpenChange={setMdPreviewFindOpen}
focusNonce={mdPreviewFindFocusNonce}
/>
)}
</div>
) : selectedFile && isHtml && htmlViewMode === 'preview' ? (
isHtmlAssetAuthLoading ? (
@@ -4213,7 +4280,20 @@ export const FilesView: React.FC<FilesViewProps> = ({ mode = 'full' }) => {
) : null}
</div>
) : isMarkdown && getMdViewMode() === 'preview' ? (
<div className="oc-file-preview h-full overflow-auto p-4" ref={markdownPreviewRef}>
<div
className="oc-file-preview h-full overflow-auto p-4"
ref={(node) => {
markdownPreviewRef.current = node;
mdFullscreenPreviewContainerRef.current = node;
}}
>
<MarkdownPreviewSearch
containerRef={mdFullscreenPreviewContainerRef}
open={mdPreviewFindOpen}
onOpenChange={setMdPreviewFindOpen}
focusNonce={mdPreviewFindFocusNonce}
className="right-4 top-16"
/>
{selectedFile ? (
<FilePreviewCommentMenu
containerRef={markdownPreviewRef}
@@ -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([]);
});
});
@@ -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<HTMLDivElement | null>;
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<MarkdownPreviewSearchProps> = ({
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<HTMLInputElement | null>(null);
const marksRef = React.useRef<HTMLElement[]>([]);
const queryRef = React.useRef(query);
queryRef.current = query;
const debounceRef = React.useRef<ReturnType<typeof setTimeout> | null>(null);
// Focus returns here when the bar closes, so Escape does not strand focus.
const returnFocusRef = React.useRef<HTMLElement | null>(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<HTMLInputElement>) => {
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 (
<div className={cn('absolute right-3 top-3 z-10 flex items-center gap-1 rounded-lg border border-border/60 bg-[var(--surface-elevated)] px-1.5 py-1 shadow-lg', className)}>
<Icon name="search" className="ml-0.5 size-3.5 text-muted-foreground" />
<Input
ref={inputRef}
value={query}
onChange={(event) => {
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"
/>
<span
className="min-w-12 px-1 text-center typography-micro text-muted-foreground tabular-nums"
aria-live="polite"
aria-label={total > 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}`
: ''}
</span>
<Button
type="button"
variant="ghost"
size="sm"
className="size-6 p-0 text-muted-foreground"
onClick={goToPrevious}
title={t('filesView.preview.find.previousAria')}
aria-label={t('filesView.preview.find.previousAria')}
disabled={total === 0}
>
<Icon name="arrow-up" className="size-3.5" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className="size-6 p-0 text-muted-foreground"
onClick={goToNext}
title={t('filesView.preview.find.nextAria')}
aria-label={t('filesView.preview.find.nextAria')}
disabled={total === 0}
>
<Icon name="arrow-down" className="size-3.5" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className="size-6 p-0 text-muted-foreground"
onClick={close}
title={t('filesView.preview.find.closeAria')}
aria-label={t('filesView.preview.find.closeAria')}
>
<Icon name="close" className="size-3.5" />
</Button>
</div>
);
};
@@ -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;
};
+6
View File
@@ -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',
+6
View File
@@ -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',
+6
View File
@@ -1405,6 +1405,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
@@ -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',
+6
View File
@@ -1435,6 +1435,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
@@ -1441,6 +1441,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
@@ -1953,6 +1953,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',
@@ -1405,6 +1405,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
@@ -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ç',
+6
View File
@@ -1405,6 +1405,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": "Перемкнутися в режим попереднього перегляду",
@@ -1405,6 +1405,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': '切换到预览模式',
@@ -1416,6 +1416,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': '切換到預覽模式',