fix(ssh): move through the remote folder browser with the arrow keys (#33914)

* fix(ssh): move through the remote folder browser with the arrow keys

The browser had no highlighted row and its filter box ignored Up/Down, so
the keys only scrolled the list. The first entry is now highlighted, the
arrows move it into view, and Enter opens it. A filter that leaves one
folder still starts on that folder, and a fully typed path starts on no
row so Enter still goes to that path. Also drops the duplicated
"No matches" line.

Fixes #31555

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Name the remote folder listbox and point at no row while a listing loads

While a listing loads only the spinner renders, so the highlight and
aria-activedescendant had been left on rows that were no longer there.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep Enter and the highlight off rows that are about to be replaced

While the committed listing loads, Enter fell back to the previous
listing's entries and could open a folder from it. While a path preview
resolves, its prior rows stay visible but are no longer targetable, so the
highlight and aria-activedescendant now clear. The filter input also gets
combobox semantics and a name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ssh): drop a moved highlight when a typed path previews the same listing

* fix(ssh): reset the folder highlight per keystroke and hold Enter until a typed path resolves

* fix(ssh): hold Enter on typed path input before its first preview

---------

Co-authored-by: Younghwan Chae <chyohw97@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Neil
2026-10-26 05:57:19 -07:00
committed by GitHub
co-authored by Claude Opus 5.5 Younghwan Chae
parent 94f562e634
commit 99631994bc
13 changed files with 455 additions and 27 deletions
@@ -0,0 +1,220 @@
// @vitest-environment happy-dom
import { act } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { RemoteFileBrowser } from './RemoteFileBrowser'
const browseDir = vi.fn(async ({ dirPath }: { dirPath: string }) => ({
entries: [
{ name: 'docs', isDirectory: true },
{ name: 'src', isDirectory: true },
{ name: 'README.md', isDirectory: false }
],
resolvedPath: dirPath === '~' ? '/home/alice' : dirPath,
pathFlavor: 'posix' as const
}))
async function flushPromises(): Promise<void> {
for (let index = 0; index < 4; index += 1) {
await Promise.resolve()
}
}
async function render(): Promise<{
container: HTMLDivElement
input: HTMLInputElement
root: Root
}> {
const container = document.createElement('div')
document.body.appendChild(container)
const root = createRoot(container)
await act(async () => {
root.render(<RemoteFileBrowser targetId="target-1" onSelect={vi.fn()} onCancel={vi.fn()} />)
await flushPromises()
})
const input = container.querySelector('input')
if (!input) {
throw new Error('Remote file browser input was not rendered')
}
return { container, input, root }
}
async function press(
input: HTMLInputElement,
key: string,
modifiers: KeyboardEventInit = {}
): Promise<void> {
await act(async () => {
input.dispatchEvent(
new KeyboardEvent('keydown', { key, bubbles: true, cancelable: true, ...modifiers })
)
await flushPromises()
})
}
async function type(input: HTMLInputElement, value: string): Promise<void> {
await act(async () => {
Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(input, value)
input.dispatchEvent(new Event('input', { bubbles: true }))
})
}
async function settlePathResolve(): Promise<void> {
await act(async () => {
vi.advanceTimersByTime(300)
await flushPromises()
})
}
function selectedRow(container: HTMLElement): string | null | undefined {
return container.querySelector('[role="option"][aria-selected="true"]')?.textContent
}
describe('RemoteFileBrowser keyboard navigation', () => {
beforeEach(() => {
globalThis.IS_REACT_ACT_ENVIRONMENT = true
browseDir.mockClear()
Object.defineProperty(window, 'api', { configurable: true, value: { ssh: { browseDir } } })
})
afterEach(() => {
vi.useRealTimers()
document.body.replaceChildren()
vi.restoreAllMocks()
})
it('highlights the first row and moves it with the arrow keys', async () => {
const { container, input, root } = await render()
expect(selectedRow(container)).toBe('docs')
expect(input.getAttribute('aria-activedescendant')).toBe(
container.querySelector('[role="option"][aria-selected="true"]')?.id
)
expect(input.getAttribute('role')).toBe('combobox')
expect(input.getAttribute('aria-controls')).toBe(
container.querySelector('[role="listbox"]')?.id
)
await press(input, 'ArrowDown')
await press(input, 'ArrowDown')
await press(input, 'ArrowDown')
expect(selectedRow(container)).toBe('README.md')
await press(input, 'ArrowUp')
expect(selectedRow(container)).toBe('src')
// Modified arrows stay with the input (selection, caret jumps).
await press(input, 'ArrowDown', { shiftKey: true })
expect(selectedRow(container)).toBe('src')
await act(async () => root.unmount())
})
it('opens the highlighted folder on Enter', async () => {
const { container, input, root } = await render()
browseDir.mockClear()
await press(input, 'ArrowDown')
await press(input, 'Enter')
expect(browseDir).toHaveBeenCalledWith({ targetId: 'target-1', dirPath: '/home/alice/src' })
expect(selectedRow(container)).toBe('docs')
await act(async () => root.unmount())
})
it('points the input at no row while the next listing loads', async () => {
const { input, root } = await render()
browseDir.mockImplementationOnce(() => new Promise(() => {}))
await press(input, 'Enter')
expect(input.hasAttribute('aria-activedescendant')).toBe(false)
await act(async () => root.unmount())
})
// Why: the previous listing's single folder would otherwise win Enter while the next one loads.
it('ignores Enter while the next listing loads', async () => {
browseDir.mockImplementationOnce(async () => ({
entries: [
{ name: 'docs', isDirectory: true },
{ name: 'README.md', isDirectory: false }
],
resolvedPath: '/home/alice',
pathFlavor: 'posix' as const
}))
const { input, root } = await render()
browseDir.mockClear()
browseDir.mockImplementationOnce(() => new Promise(() => {}))
await press(input, 'Enter')
await press(input, 'Enter')
expect(browseDir).toHaveBeenCalledTimes(1)
await act(async () => root.unmount())
})
// Why: the cached home listing comes back as the same array under `~/`, which must not keep the moved row.
it('commits a typed path instead of a row highlighted before typing it', async () => {
vi.useFakeTimers()
const { input, root } = await render()
await press(input, 'ArrowDown')
browseDir.mockClear()
await type(input, '~/')
await settlePathResolve()
await press(input, 'Enter')
// Committing `~/` reopens the cached home listing and clears the typed path.
expect(input.value).toBe('')
expect(browseDir).not.toHaveBeenCalledWith({
targetId: 'target-1',
dirPath: '/home/alice/src'
})
await act(async () => root.unmount())
})
// Why: until the debounce resolves `docs/`, the preview and its moved highlight still show `~/`.
it('ignores Enter on a typed directory that has not resolved yet', async () => {
vi.useFakeTimers()
const { input, root } = await render()
await type(input, '~/')
await settlePathResolve()
await press(input, 'ArrowDown')
await press(input, 'ArrowDown')
browseDir.mockClear()
await type(input, '~/docs/')
await press(input, 'Enter')
expect(browseDir).not.toHaveBeenCalled()
await settlePathResolve()
await press(input, 'Enter')
expect(browseDir).toHaveBeenLastCalledWith({
targetId: 'target-1',
dirPath: '/home/alice/docs'
})
await act(async () => root.unmount())
})
// Why: before the first preview, `.` still filters the old rows, so the default highlight is a dotfolder.
it('ignores Enter on path input that has no preview yet', async () => {
vi.useFakeTimers()
browseDir.mockImplementationOnce(async () => ({
entries: [
{ name: '.config', isDirectory: true },
{ name: '.ssh', isDirectory: true }
],
resolvedPath: '/home/alice',
pathFlavor: 'posix' as const
}))
const { input, root } = await render()
browseDir.mockClear()
await type(input, '.')
await press(input, 'Enter')
expect(browseDir).not.toHaveBeenCalled()
await act(async () => root.unmount())
})
})
@@ -1,12 +1,14 @@
import { ImeInput } from '@/lib/ime-text-field'
import type React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useCallback, useEffect, useId, useMemo, useRef, useState } from 'react'
import { LoaderCircle, Search } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { filterEntries, joinPath, parentPath, type DirEntry } from './remote-file-browser-helpers'
import { RemoteFileBrowserBreadcrumbs } from './RemoteFileBrowserBreadcrumbs'
import { RemoteFileBrowserEntryList } from './RemoteFileBrowserEntryList'
import { initialRemoteFileBrowserHighlight } from './remote-file-browser-highlight'
import type { PreviewState } from './remote-file-browser-path-preview-resolver'
import { useRemoteFileBrowserFilterKeyCommands } from './use-remote-file-browser-filter-key-commands'
import { useRemoteFileBrowserListing } from './use-remote-file-browser-listing'
import { useRemoteFileBrowserPathPreview } from './use-remote-file-browser-path-preview'
@@ -24,6 +26,7 @@ type RemoteFileBrowserProps = (
}
const FILE_HINT_MS = 2000
const NO_ENTRIES: readonly DirEntry[] = []
const FILE_HINT_TEXT = "Files can't be opened as a project"
export function RemoteFileBrowser({
@@ -40,6 +43,15 @@ export function RemoteFileBrowser({
const [error, setError] = useState<string | null>(null)
const [filter, setFilter] = useState('')
const [fileHint, setFileHint] = useState(false)
// Tied to the list, preview and text it was moved on, so any new listing, preview or keystroke starts
// from its own default (a cached listing reappears as the same array; a typed path waits on a debounce).
const [highlight, setHighlight] = useState<{
entries: readonly DirEntry[]
preview: PreviewState | null
filter: string
index: number
} | null>(null)
const listboxId = useId()
const genRef = useRef(0)
const inputRef = useRef<HTMLInputElement>(null)
const fileHintTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
@@ -61,7 +73,8 @@ export function RemoteFileBrowser({
handleInputPaste,
discardPreview,
resetPreviewForNavigation,
cancelPreviewWork
cancelPreviewWork,
previewMatchesInput
} = useRemoteFileBrowserPathPreview({
resolvedPath,
pathFlavor,
@@ -156,6 +169,24 @@ export function RemoteFileBrowser({
}, [resolvedPath, navigate, pathFlavor])
const filteredEntries = useMemo(() => filterEntries(entries, filter), [entries, filter])
const previewFilteredEntries = useMemo(
() => (preview ? filterEntries(preview.entries, preview.filter) : null),
[preview]
)
// Why: while a listing loads only the spinner renders, so the prior rows can't be targeted.
const displayEntries = loading ? NO_ENTRIES : (previewFilteredEntries ?? filteredEntries)
// Why: a resolving preview keeps its prior rows on screen, but they are no longer targetable.
const activeIndex = preview?.loading
? -1
: highlight?.entries === displayEntries &&
highlight.preview === preview &&
highlight.filter === filter
? highlight.index
: initialRemoteFileBrowserHighlight(displayEntries, preview !== null && preview.filter === '')
const moveHighlight = useCallback(
(index: number) => setHighlight({ entries: displayEntries, preview, filter, index }),
[displayEntries, preview, filter]
)
const triggerFileHint = useCallback(() => {
if (fileHintTimerRef.current) {
@@ -211,10 +242,16 @@ export function RemoteFileBrowser({
)
const handleFilterKeyDown = useRemoteFileBrowserFilterKeyCommands({
loading,
filter,
setFilter,
filteredEntries,
displayEntries,
activeIndex,
moveHighlight,
listParentPath,
preview,
previewMatchesInput,
discardPreview,
resolvedPath,
pathFlavor,
@@ -247,6 +284,13 @@ export function RemoteFileBrowser({
<ImeInput
ref={inputRef}
type="text"
role="combobox"
aria-expanded
aria-autocomplete="list"
aria-label={translate(
'auto.components.sidebar.RemoteFileBrowser.inputLabel',
'Filter or path'
)}
autoFocus
value={filter}
onChange={(e) => handleInputChange(e.target.value)}
@@ -256,6 +300,8 @@ export function RemoteFileBrowser({
'auto.components.sidebar.RemoteFileBrowser.2300612806',
'Type to filter or enter a path…'
)}
aria-controls={listboxId}
aria-activedescendant={activeIndex >= 0 ? `${listboxId}-${activeIndex}` : undefined}
aria-invalid={!!preview?.error}
aria-describedby={preview?.error ? 'remote-file-browser-path-error' : undefined}
className={cn(
@@ -283,7 +329,9 @@ export function RemoteFileBrowser({
loading={loading}
error={error}
entries={entries}
filteredEntries={filteredEntries}
displayEntries={displayEntries}
activeIndex={activeIndex}
listboxId={listboxId}
filter={filter}
preview={preview}
isPreviewActive={isPreviewActive}
@@ -1,10 +1,9 @@
import type React from 'react'
import { useMemo, type RefObject } from 'react'
import { useEffect, useRef, type RefObject } from 'react'
import { ChevronRight, Folder, LoaderCircle } from 'lucide-react'
import { cn } from '@/lib/utils'
import { FileTypeIcon } from '@/components/FileTypeIcon'
import {
filterEntries,
isRemoteFileBrowserPathResolveTextTooLarge,
type DirEntry
} from './remote-file-browser-helpers'
@@ -15,7 +14,9 @@ type RemoteFileBrowserEntryListProps = {
loading: boolean
error: string | null
entries: DirEntry[]
filteredEntries: DirEntry[]
displayEntries: readonly DirEntry[]
activeIndex: number
listboxId: string
filter: string
preview: PreviewState | null
isPreviewActive: boolean
@@ -28,7 +29,9 @@ export function RemoteFileBrowserEntryList({
loading,
error,
entries,
filteredEntries,
displayEntries,
activeIndex,
listboxId,
filter,
preview,
isPreviewActive,
@@ -36,12 +39,17 @@ export function RemoteFileBrowserEntryList({
handleRowClick,
handleRowDoubleClick
}: RemoteFileBrowserEntryListProps): React.JSX.Element {
const previewFilteredEntries = useMemo(
() => (preview ? filterEntries(preview.entries, preview.filter) : []),
[preview]
)
const scrollRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (activeIndex < 0) {
return
}
// Optional call: happy-dom has no scrollIntoView.
scrollRef.current
?.querySelector(`[data-entry-index="${activeIndex}"]`)
?.scrollIntoView?.({ block: 'nearest' })
}, [activeIndex, displayEntries])
const displayEntries = isPreviewActive ? previewFilteredEntries : filteredEntries
const displayEmptyDirCopy = isPreviewActive
? `${preview!.resolvedPath} is empty`
: 'Empty directory'
@@ -59,7 +67,16 @@ export function RemoteFileBrowserEntryList({
return (
<div className="border border-border rounded-md overflow-hidden bg-background">
<div className="h-[240px] overflow-y-auto scrollbar-sleek">
<div
ref={scrollRef}
id={listboxId}
role="listbox"
aria-label={translate(
'auto.components.sidebar.RemoteFileBrowser.entriesLabel',
'Folder entries'
)}
className="h-[240px] overflow-y-auto scrollbar-sleek"
>
{loading ? (
<div className="flex items-center justify-center h-full">
<LoaderCircle className="size-5 animate-spin text-muted-foreground" />
@@ -85,13 +102,19 @@ export function RemoteFileBrowserEntryList({
// Filtered-out entries need different copy from a directory that is genuinely empty.
<div className="flex items-center justify-center h-full">
<p className="text-xs text-muted-foreground">{displayNoMatchesCopy}</p>
<p className="text-xs text-muted-foreground">{displayNoMatchesCopy}</p>
</div>
) : (
displayEntries.map((entry) => {
displayEntries.map((entry, index) => {
const isActive = index === activeIndex
return (
<button
key={entry.name}
id={`${listboxId}-${index}`}
role="option"
aria-selected={isActive}
data-selected={isActive}
data-entry-index={index}
tabIndex={-1}
type="button"
onClick={() => handleRowClick(entry)}
onDoubleClick={() => handleRowDoubleClick(entry)}
@@ -100,7 +123,8 @@ export function RemoteFileBrowserEntryList({
inputRef.current?.focus()
}}
className={cn(
'w-full flex items-center gap-2 px-3 py-1.5 text-xs text-left transition-colors cursor-pointer',
// Why `jump-palette-item`: flat accent is invisible as a keyboard cursor on light dialogs.
'jump-palette-item w-full flex items-center gap-2 px-3 py-1.5 text-xs text-left transition-colors cursor-pointer',
'hover:bg-accent/60'
)}
>
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import {
initialRemoteFileBrowserHighlight,
stepRemoteFileBrowserHighlight
} from './remote-file-browser-highlight'
const folder = (name: string) => ({ name, isDirectory: true })
const file = (name: string) => ({ name, isDirectory: false })
describe('initialRemoteFileBrowserHighlight', () => {
it('starts on the first row', () => {
expect(initialRemoteFileBrowserHighlight([folder('a'), folder('b')], false)).toBe(0)
})
// Why: Enter on a filter that leaves one folder has always opened it, even past files.
it('starts on the only folder a filter leaves', () => {
expect(initialRemoteFileBrowserHighlight([file('src.md'), folder('src')], false)).toBe(1)
})
it('starts on no row when the list is empty or the typed path is itself the target', () => {
expect(initialRemoteFileBrowserHighlight([], false)).toBe(-1)
expect(initialRemoteFileBrowserHighlight([folder('a')], true)).toBe(-1)
})
})
describe('stepRemoteFileBrowserHighlight', () => {
it('moves within the list and stops at both ends', () => {
expect(stepRemoteFileBrowserHighlight(0, 1, 3)).toBe(1)
expect(stepRemoteFileBrowserHighlight(2, 1, 3)).toBe(2)
expect(stepRemoteFileBrowserHighlight(0, -1, 3)).toBe(0)
})
it('enters the list from no row at the end the key points to', () => {
expect(stepRemoteFileBrowserHighlight(-1, 1, 3)).toBe(0)
expect(stepRemoteFileBrowserHighlight(-1, -1, 3)).toBe(2)
})
})
@@ -0,0 +1,27 @@
import { decideEnterAction, type DirEntry } from './remote-file-browser-helpers'
/** -1 means no row: Enter then acts on the typed path, not a listed entry. */
export function initialRemoteFileBrowserHighlight(
entries: readonly DirEntry[],
typedPathIsTarget: boolean
): number {
if (entries.length === 0 || typedPathIsTarget) {
return -1
}
const action = decideEnterAction([...entries])
return action.type === 'navigate' ? entries.findIndex((entry) => entry.name === action.name) : 0
}
export function stepRemoteFileBrowserHighlight(
index: number,
delta: 1 | -1,
length: number
): number {
if (length === 0) {
return -1
}
if (index < 0) {
return delta === 1 ? 0 : length - 1
}
return Math.min(Math.max(index + delta, 0), length - 1)
}
@@ -7,14 +7,22 @@ import {
parsePathInput,
type DirEntry
} from './remote-file-browser-helpers'
import { stepRemoteFileBrowserHighlight } from './remote-file-browser-highlight'
import { isImeOwnedKeyboardEvent } from '@/lib/ime-composition-keyboard-event'
import type { PreviewState } from './remote-file-browser-path-preview-resolver'
import type { FilesystemPathFlavor } from '../../../../shared/filesystem-entry-types'
export type RemoteFileBrowserFilterKeyCommandsArgs = {
loading: boolean
filter: string
setFilter: Dispatch<SetStateAction<string>>
filteredEntries: DirEntry[]
displayEntries: readonly DirEntry[]
activeIndex: number
moveHighlight: (index: number) => void
listParentPath: string
preview: PreviewState | null
previewMatchesInput: (raw: string) => boolean
discardPreview: () => void
resolvedPath: string
pathFlavor: FilesystemPathFlavor
@@ -27,10 +35,16 @@ export type RemoteFileBrowserFilterKeyCommandsArgs = {
}
export function useRemoteFileBrowserFilterKeyCommands({
loading,
filter,
setFilter,
filteredEntries,
displayEntries,
activeIndex,
moveHighlight,
listParentPath,
preview,
previewMatchesInput,
discardPreview,
resolvedPath,
pathFlavor,
@@ -43,12 +57,39 @@ export function useRemoteFileBrowserFilterKeyCommands({
}: RemoteFileBrowserFilterKeyCommandsArgs): (e: KeyboardEvent<HTMLInputElement>) => void {
return useCallback(
(e: KeyboardEvent<HTMLInputElement>) => {
const modified = e.shiftKey || e.metaKey || e.ctrlKey || e.altKey
if (
(e.key === 'ArrowDown' || e.key === 'ArrowUp') &&
!modified &&
!isImeOwnedKeyboardEvent(e)
) {
if (displayEntries.length > 0 && !preview?.loading) {
e.preventDefault()
const delta = e.key === 'ArrowDown' ? 1 : -1
moveHighlight(stepRemoteFileBrowserHighlight(activeIndex, delta, displayEntries.length))
}
return
}
if (e.key === 'Enter') {
if (preview) {
if (preview.error || preview.loading) {
e.preventDefault()
return
// Why: until the debounce resolves typed path input, the rows on screen belong to another directory.
const pathPending =
parsePathInput(filter, pathFlavor).mode === 'path' &&
(!preview || !previewMatchesInput(filter))
if (preview?.error || preview?.loading || pathPending) {
e.preventDefault()
return
}
const highlighted = displayEntries[activeIndex]
if (highlighted) {
e.preventDefault()
if (highlighted.isDirectory) {
navigate(joinPath(listParentPath, highlighted.name, pathFlavor))
} else {
triggerFileHint()
}
return
}
if (preview) {
const parsed = parsePathInput(filter, pathFlavor)
// Without a trailing filter, commit the resolved preview directory itself.
if (parsed.mode === 'path' && parsed.trailingFilter === '') {
@@ -70,6 +111,11 @@ export function useRemoteFileBrowserFilterKeyCommands({
}
return
}
// Why: while the next listing loads, filteredEntries still holds the previous directory.
if (loading) {
e.preventDefault()
return
}
const action = decideEnterAction(filteredEntries)
if (action.type === 'navigate') {
e.preventDefault()
@@ -100,9 +146,15 @@ export function useRemoteFileBrowserFilterKeyCommands({
}
},
[
loading,
filter,
filteredEntries,
displayEntries,
activeIndex,
moveHighlight,
listParentPath,
preview,
previewMatchesInput,
navigate,
navigateInto,
navigateUp,
@@ -43,6 +43,8 @@ export type RemoteFileBrowserPathPreview = {
resetPreviewForNavigation: () => void
/** Detach: invalidate in-flight preview work and clear its timers. */
cancelPreviewWork: () => void
/** False while a typed path's directory part still waits on the resolve debounce. */
previewMatchesInput: (raw: string) => boolean
}
export function useRemoteFileBrowserPathPreview({
@@ -195,12 +197,18 @@ export function useRemoteFileBrowserPathPreview({
[resolvePathInput, pathFlavor, inputRef]
)
const previewMatchesInput = useCallback(
(raw: string) => committedPrefix(raw) === lastCommittedPrefixRef.current,
[]
)
return {
preview,
handleInputChange,
handleInputPaste,
discardPreview,
resetPreviewForNavigation,
cancelPreviewWork
cancelPreviewWork,
previewMatchesInput
}
}
+3 -1
View File
@@ -5944,7 +5944,9 @@
"51001182e3": "Empty directory",
"971d85cc84": "Opens as a project on this host · {{value0}}",
"00c4235c10": "No matches for '{{value0}}'",
"largeInputNoMatches": "No matches for this long input"
"largeInputNoMatches": "No matches for this long input",
"entriesLabel": "Folder entries",
"inputLabel": "Filter or path"
},
"SleepTwinWorkspaceDialog": {
"title": "Sleep Workspace",
+3 -1
View File
@@ -5582,7 +5582,9 @@
"51001182e3": "Directorio vacío",
"971d85cc84": "Se abre como proyecto en este host · {{value0}}",
"00c4235c10": "No hay coincidencias para '{{value0}}'",
"largeInputNoMatches": "No hay coincidencias para esta entrada larga"
"largeInputNoMatches": "No hay coincidencias para esta entrada larga",
"entriesLabel": "Entradas de la carpeta",
"inputLabel": "Filtro o ruta"
},
"SleepTwinWorkspaceDialog": {
"title": "Sleep Workspace",
+3 -1
View File
@@ -5745,7 +5745,9 @@
"51001182e3": "Répertoire vide",
"971d85cc84": "S'ouvre comme projet sur cet hôte · {{value0}}",
"00c4235c10": "Aucun résultat pour « {{value0}} »",
"largeInputNoMatches": "Aucun résultat pour cette saisie longue"
"largeInputNoMatches": "Aucun résultat pour cette saisie longue",
"entriesLabel": "Éléments du dossier",
"inputLabel": "Filtre ou chemin"
},
"SleepTwinWorkspaceDialog": {
"title": "Sleep Workspace",
+3 -1
View File
@@ -5548,7 +5548,9 @@
"51001182e3": "空のディレクトリ",
"971d85cc84": "このホスト上のプロジェクトとして開きます · {{value0}}",
"00c4235c10": "「{{value0}}」に一致するものはありません",
"largeInputNoMatches": "この長い入力に一致するものはありません"
"largeInputNoMatches": "この長い入力に一致するものはありません",
"entriesLabel": "フォルダーの項目",
"inputLabel": "フィルターまたはパス"
},
"SleepTwinWorkspaceDialog": {
"title": "Sleep Workspace",
+3 -1
View File
@@ -5548,7 +5548,9 @@
"51001182e3": "빈 디렉터리",
"971d85cc84": "이 호스트에서 프로젝트로 열립니다 · {{value0}}",
"00c4235c10": "'{{value0}}'과(와) 일치하는 항목이 없습니다.",
"largeInputNoMatches": "이 긴 입력과 일치하는 항목 없음"
"largeInputNoMatches": "이 긴 입력과 일치하는 항목 없음",
"entriesLabel": "폴더 항목",
"inputLabel": "필터 또는 경로"
},
"SleepTwinWorkspaceDialog": {
"title": "Sleep Workspace",
+3 -1
View File
@@ -5548,7 +5548,9 @@
"51001182e3": "空目录",
"971d85cc84": "作为远程项目打开 · {{value0}}",
"00c4235c10": "没有匹配“{{value0}}”",
"largeInputNoMatches": "没有匹配此长输入的结果"
"largeInputNoMatches": "没有匹配此长输入的结果",
"entriesLabel": "文件夹条目",
"inputLabel": "筛选或路径"
},
"SleepTwinWorkspaceDialog": {
"title": "Sleep Workspace",