diff --git a/docs/design/0161-file-explorer-search-narrow.svg b/docs/design/0161-file-explorer-search-narrow.svg new file mode 100644 index 00000000..9df51b1f --- /dev/null +++ b/docs/design/0161-file-explorer-search-narrow.svg @@ -0,0 +1,16 @@ + + + + + + + + Search: rn + › render.ts + renderer.test.ts + Preview · render.ts + export const example = 42; + j/k move · / filter · Enter open + + + diff --git a/docs/design/0161-file-explorer-search-wide.svg b/docs/design/0161-file-explorer-search-wide.svg new file mode 100644 index 00000000..4b471cfb --- /dev/null +++ b/docs/design/0161-file-explorer-search-wide.svg @@ -0,0 +1,16 @@ + + + + + + + + Search: rn + › render.ts + renderer.test.ts + Preview · render.ts + export const example = 42; + j/k move · / filter · Enter open + + + diff --git a/docs/design/0161-file-explorer-search.md b/docs/design/0161-file-explorer-search.md new file mode 100644 index 00000000..411ebcc3 --- /dev/null +++ b/docs/design/0161-file-explorer-search.md @@ -0,0 +1,314 @@ +--- +title: "DX-0161 - Sticky explorer search" +legend: "DX" +lane: "design" +issue: "https://github.com/flyingrobots/jedit/issues/340" +status: "active" +owners: + - "@flyingrobots" +created: "2026-10-14" +updated: "2026-10-14" +--- + +# DX-0161 - Sticky explorer search + +## Linked Issue + +- https://github.com/flyingrobots/jedit/issues/340 + +## Decision Summary + +Give the Files drawer modal navigation, a sticky editable search field, pointer +hover/selection, read-only file previews, and live fuzzy filtering +of the directory entries already loaded by the workspace. The query is a +session-only UI projection; there is no persistent index, SQLite, recursive +scan, semantic retrieval, or symbol extraction in this cycle. + +## Sponsored Human + +A keyboard user opens Files and wants to immediately type a partial file name +or relative path, see matching entries, and open one without switching modes. + +## Sponsored Agent + +An agent needs inspectable query/cursor state and one shared filtered-entry +projection for painting, navigation, opening, and pointer hit-testing. + +## Hill + +Ctrl+B opens navigation; / enters the pinned filter. j/k, arrows, and clicks +select results and show read-only previews without replacing the active buffer. +Enter opens exactly the selected match through the existing file-open path. + +## Current Truth + +The Files drawer renders only a scrollable list. Its focused keys perform +navigation and directory actions; there is no input or query state. See +[viewer-drawers.ts#14:5d7b0957a2511ac28b9ec9f47c9e6c432f89deea](https://github.com/flyingrobots/jedit/blob/5d7b0957a2511ac28b9ec9f47c9e6c432f89deea/src/app/workspace/viewer-drawers.ts#L14) +and [file-tree.ts#47:5d7b0957a2511ac28b9ec9f47c9e6c432f89deea](https://github.com/flyingrobots/jedit/blob/5d7b0957a2511ac28b9ec9f47c9e6c432f89deea/src/app/workspace/file-tree.ts#L47). + +Graft exposes per-file outline projections through `GraftSessionPort`, not a +ready-made workspace-wide symbol inventory. This PR is stacked on #339. + +## Problem + +Opening the explorer does not provide a place to type. Printable keys currently +perform actions such as navigation, refresh, quit, or perf toggling. + +## Scope + +- Pinned single-line input, automatic focus, visible caret, horizontal query scrolling. +- Case-insensitive ordered-subsequence matching of loaded names and relative paths. +- Shared filtered-entry identity for render, keys, wheel, click, and file opening. +- Query editing, empty results, directory transition, and focus/close behavior. + +## Non-Goals + +- Recursive workspace inventory, semantic retrieval, Graft symbol scan, SQLite, persistence. +- Leader configuration/dispatch or changes to authoritative Jim/Echo text state. + +## User Experience / Product Shape + +The refinement replaces automatic input editing with explicit explorer modality. +Ctrl+B opens Files in navigation mode. j/k and Up/Down select results; `/` enters +filter editing. Clicking the input also enters editing. Esc or Enter in editing +returns to navigation while preserving the query. Esc in navigation closes the +drawer. Ctrl+B closes from either mode. Reopening starts in navigation with the +session query retained. F2 and Tab leave filter editing before changing focus. + +The search field remains pinned and uses a distinct theme background. Its caret +is visible only while editing and Files owns focus. Query edits select the first +match. Matched filename characters retain the accent and bold styling. Directory +changes clear the query. j/k/r/Backspace/h/l regain list actions in navigation; +Ctrl+R refreshes and Alt+Up goes to the parent in either mode. + +Pointer motion highlights the row under the cursor without changing selection +or loading a preview. Hover and selection have distinct theme backgrounds; +selection takes precedence if the cursor is over the selected row. Clicking a +row selects it and returns to navigation. A file selection previews it in the +main viewer while focus remains on Files. Clicking a directory only selects it; +Enter enters it. Enter on a file opens it through the native Echo path and moves +focus to the editor. Closing or leaving Files restores the existing editor. + +Preview is an explicitly labelled, read-only, bounded projection. The current +active buffer and other open buffer projections are preferred so unsaved text is +visible. Otherwise a filesystem adapter reads at most 64 KiB and 200 lines. +Binary, directory, unavailable, loading, and truncated previews are labelled. +Preview does not create an Echo buffer, change text authority, edit, or save. +Request identity and selected source entry reject late results. Selecting a +new item replaces the previous preview with loading immediately; no matches or +non-file selection clears it. No persistent preview cache is introduced. + +Terminal input must deliver Unicode scalars and modified arrows through a +Jedit boundary adapter; all-motion mouse reporting enables hover. Windows path +normalization occurs at the filesystem boundary with a portable search-path +field, leaving native paths intact for opening. Existing review findings on +these boundaries are included in this refinement. + +### User Journey + +Open Files → select with j/k or pointer → inspect preview → Enter opens. +Press / → type query → Esc → choose a filtered result. Backspace +or Ctrl+U restores broader results; Esc closes and returns to the normal pane. + +### Wide UI Mockup + +100×32 workspace, existing drawer/theme. The input row and separator stay pinned. + +![Wide search field](0161-file-explorer-search-wide.svg) + +### Narrow UI Mockup + +60×16 workspace. The query scrolls horizontally; the selected row remains visible. + +![Narrow search field](0161-file-explorer-search-narrow.svg) + +### Accessibility Considerations + +Text label, visible caret, visible selected result, and an explicit no-match +message. Keyboard-only editing and opening; focus is never inferred from color +alone. Pointer mapping uses the same geometry as rendering. Existing minimum +terminal geometry remains unchanged. + +## Runtime / API Contract + +`WorkspaceModel.fileSearch` holds input and cursor coordinates; +`fileSearchEditing` declares filter mode and `fileHoveredIndex` is independent +of selection. `fileExplorerPreview` carries a selected entry and request id. A pure selector +returns visible entries with their source indexes; `selectedIndex` retains the +loaded-entry index so existing file-opening identity is preserved. Query changes +select the first match or no selection. All filtered actions use that selector. +The input handler runs before plain global shortcuts only in filter mode; +Ctrl+C, close, pane switching, and Settings retain their existing routes. + +## Lower Modes + +Input, query, selected rows, and no-match text remain visible without color. +The caret uses the existing theme token. Small viewports clip safely. No Graft +or optional database adapter is required; unreadable directory errors retain +existing file-system feedback and preserve the prior query/list. + +## Data / State Model + +| Category | Description | +| --- | --- | +| Source of truth | Loaded FileEntry list, session mode/query/caret, selected entry, and bounded preview observation. | +| Derived state | Filtered entries, selection position, text window, list scroll offset. | +| Invalid states | Controls in query; opening an entry excluded by the current query. | +| Reset behavior | Successful directory change clears query; closing preserves it. | +| Persistence | None. A restart begins with an empty query and navigation mode. | +| Causal boundary | Disposable presentation over loaded file-system entries; no authoritative index or text mutation. | + +## Accessibility Posture + +Input focus stays on Files during arrow selection. Opening a file returns focus +to the existing editor/open path. Closing uses the existing pane restoration. +Empty results are readable rather than silent. Search is scoped to the loaded +directory, so it does not claim a complete workspace inventory. Hover has a +quiet background; selection has an accent background with contrast-adjusted +text. Existing theme variants keep them distinct. + +## Localization / Directionality Posture + +New search label, placeholder, empty-results text, and footer hints use catalog +keys for every authored locale and the generated mirror locale. File paths keep +their literal order; existing drawer directionality remains. Width calculations +use terminal display-cell lengths, and Unicode input boundaries are preserved. + +## Agent Inspectability / Explainability Posture + +Inspect query/caret, selected source index, and the visible-entry selector. +Runtime tests assert the requested path rather than a fabricated open result. +Render tests inspect fixed row positions and cursor tokens. Live PTY exercises +real input decoding and the native-host file-open route. + +## Linked Invariants + +- Source lists and buffers are not optimistically mutated by filtering. +- Derived UI indexes do not become causal or filesystem authority. +- One list projection and geometry serve all result interactions. +- Tests prove real runtime behavior and rendering. + +## Design Alternatives Considered + +An input-only field would not help locate files. A small deterministic fuzzy +filter requires no index or additional service. A repository crawler or symbol +scan needs bounded asynchronous inventory and stale-result rules, so it is +tracked separately in #341. SQLite has no demonstrated need in this slice. + +## Decision + +Ship the pinned input and ephemeral fuzzy filtering over loaded entries. Keep +workspace-wide and symbol search separate. + +## Implementation Slices + +- [x] Slice 1: Input state, fuzzy projection, and keyboard/open behavior. +- [x] Slice 2: Sticky input/caret rendering, pointer/wheel mapping, and localized help. +- [x] Slice 3: Runtime/render/native-host verification and documentation. +- [x] Slice 4: Explicit navigation/filter modes and mode-aware help. +- [x] Slice 5: Pointer hover, click selection, and distinct row colors. +- [x] Slice 6: Bounded read-only viewer previews and stale-result rejection. +- [x] Slice 7: Real input boundary fixes, native verification, and refined docs. + +## Tests To Write First + +- [x] Ctrl+B focuses a visible input; printable shortcut keys type query text. +- [x] Fuzzy matching, no matches, editing, and Unicode caret movement behave correctly. +- [x] Arrows/Enter/click open the displayed match rather than a hidden source entry. +- [x] Header stays pinned while results scroll; mouse/wheel use the filtered list. +- [x] Close/reopen, Settings, pane switching, refresh, and directory transitions preserve the contract. + +## Acceptance Criteria + +- [x] Input is pinned, focused, and visibly editable on open. +- [x] Loaded paths filter live with no persistent cache or index. +- [x] All result interactions share the same projection and geometry. +- [x] Empty results cannot open a hidden file. +- [x] Full repository checks, localized rendering, and live PTY validation pass. + +## Validation Plan + +```bash +npm run check +npm run ci:verify-shards +``` + +## Playback / Witness + +Ctrl+B → / → type partial name → Esc → j/k or click → inspect preview → Enter. Repeat with no matches, clear with +Ctrl+U, edit in the middle, scroll a long list, and click a filtered row. Inspect +at 100×32 and 60×16, including the real native-host file-open path. + +## Risks + +Existing j/k/r/Backspace navigation is preserved in navigation and becomes +text editing only in filter mode; footer help must match. Filtering changes displayed indexes, so stale +raw-index pointer calculations can open the wrong file. Unknown-symbol and +workspace-inventory claims must remain outside this feature. + +## Follow-On Debt + +[Issue #341](https://github.com/flyingrobots/jedit/issues/341) owns evaluation of +workspace-wide file, semantic, and Graft symbol search with disposable indexes. + +## Retrospective + +Implemented with one shared filtered projection retaining original entry indexes. +The input owns printable keys while focus remains on Files; pointer geometry +reserves the same pinned rows as painting. Match highlights use the filter's +Unicode character positions, preferring filename matches when possible. + +The initial red tests exposed the missing input state and later the missing +visual distinction. Existing list-navigation and pointer tests were updated for +arrow navigation and the two extra pinned rows. Thirteen focused runtime/render +tests pass, including Japanese, Arabic, and mirrored labels, wide-character +caret mapping, and distinct input backgrounds across 16 built-in theme variants. +The full repository check passes, and shard coverage includes all 131 specs. + +Live native-host PTY sessions at 100×32 and 60×16 confirmed decoded input, +focused query editing, no-match Enter safety, and the selected fixture's actual +Echo import result with its expected source content. Terminal snapshots were +inspected at both sizes; the input remained at the top and matched characters +were accented. Quality reports no regressions and its debt baseline is unchanged. + +No persistent query, recursive inventory, symbols, semantic retrieval, or cache +was added. These remain explicit follow-on work in #341. This change is stacked +on the static-title removal in #339; neither PR is merged by this cycle. + +### Refinement acceptance (2026-10-14) + +- [x] j/k navigates after opening; slash/click edits; Esc returns to navigation. +- [x] Hover never selects or reads a file; click selects; colors remain distinct. +- [x] Selected files preview without changing buffers, dirty text, or authority. +- [x] Bounded, unavailable, binary, stale-result, and focus-return cases pass. +- [x] Actual terminal Unicode, modified arrows, and hover packets are exercised. +- [x] Full local checks and CI pass; documentation reflects final behavior. + +Refinement retrospective: implemented in `3a940469`, replacing always-editing +focus with navigation/filter modes. Mouse click now selects, and hover stays +independent. Selection shows a bounded, read-only preview; Enter remains the +explicit native Echo open boundary. The previous active buffer is preserved. +Preview results carry original entry identity and a request id, rejecting stale +selection and closed-drawer results. Existing open buffers provide live text. + +The focused tests prove navigation and filter transitions, filtered pointer +geometry, independent hover, preview rendering, stale-result rejection, unsaved +text preservation, bounded UTF-8 reads, binary/unavailable handling, and a +nonblocking named-pipe case. Event-bus tests exercise the production input +adapter with Unicode, modified arrows, split CSI, lone Escape, and SGR hover. +Portable paths are normalized at the filesystem boundary. Selection/hover stay +distinct across 16 theme variants, with selected text checked for contrast. + +The full local check passes: 855 JavaScript tests, 12 existing skips, and all 99 +native tests. Quality has no regressions and shard coverage includes 134 specs. +Live 100×32 and 60×16 PTYs confirm j/k navigation, hover without selection or +preview reads, click selection/preview, labelled main-viewer content, real +Unicode filtering/opening, and modified Alt+Up input. Terminal snapshots were +visually inspected at both sizes. The narrow witness opened the selected file +through Echo only after Enter. + +No persistent index/cache, workspace crawl, semantic retrieval, or Graft symbol +scan was added; #341 remains the follow-on. No buffer authority was delegated +to the preview. PR #342 remains stacked on #339 and requires separate merge +authorization. CI completion is recorded in the PR. diff --git a/docs/jim-component-ownership.md b/docs/jim-component-ownership.md index 63c26c7e..c4e32f74 100644 --- a/docs/jim-component-ownership.md +++ b/docs/jim-component-ownership.md @@ -285,6 +285,20 @@ the meaning of a checkpoint declaration, while Echo owns the Tick and receipt that witness its persistence. Jedit owns a screen cache, while neither that cache nor the screen becomes text authority. +The current Files drawer search and read-only preview are transient Jedit +presentation projections. Navigation/filter mode, query/caret, hover, and +selection are session-only shell state; they are not persisted or replayed as +Jim buffer-search state. The filter creates no database or authoritative +inventory. Selection previews bounded filesystem content or an already open +buffer projection without replacing the active buffer, editing, saving, or +creating an Echo buffer. Entry identity and request identity reject stale +preview results. Native file paths remain intact while portable search paths +are normalized at the filesystem boundary. Opening explicitly with Enter +continues through the native Echo text-import path. +[DX-0161](design/0161-file-explorer-search.md) owns this bounded implementation; +a future workspace or symbol index must remain a rebuildable projection and +declare its observation basis and staleness policy. + > **Intuition to carry forward:** semantic ownership and storage authority are > different powers. Jim decides what command to perform, the text capability > defines what the text operation means, and Echo decides whether their diff --git a/docs/topics/ui/drawers-and-panels.md b/docs/topics/ui/drawers-and-panels.md index 671f4444..a9b8029d 100644 --- a/docs/topics/ui/drawers-and-panels.md +++ b/docs/topics/ui/drawers-and-panels.md @@ -23,11 +23,57 @@ enough for detailed explanations. ## Files Drawer -Open the files drawer with `ctrl+b`. +Open the files drawer with `Ctrl+B`. It starts in navigation mode. The sticky +search field has a distinct background, but takes keyboard input only after `/` +or a click on the field. Its caret appears while editing. -The files drawer is for browsing workspace entries. It should share file-opening -behavior with `:edit`: selecting a file opens it, and selecting a directory -enters that directory. +| Navigation key | Action | +| --- | --- | +| `j` / `k`, `Up` / `Down` | Select a displayed result and preview a file. | +| `/` | Edit the filter. | +| `Enter` / `l` | Open the selected file for editing, or enter a directory. | +| `Backspace` / `h`, `Alt+Up` | Enter the parent directory. | +| `r` / `Ctrl+R` | Refresh the current directory. | +| `Ctrl+B` / `Esc` | Close the drawer. | +| `Tab` | Cycle visible panes. | + +| Filter key | Action | +| --- | --- | +| Printable characters | Fuzzy-filter loaded entries as you type. | +| `Left` / `Right`, `Home` / `End` | Move the query caret. | +| `Backspace` / `Delete` | Edit the query. | +| `Ctrl+U` | Clear the query. | +| `Esc` / `Enter` | Return to navigation and retain the query. | +| `Up` / `Down` | Select a displayed result. | +| `Ctrl+B` | Close the drawer. | + +Matching is case-insensitive and accepts characters in order, so `rnt` can match +`render-tree.ts`. It preserves the directory's existing order. Matched filename +characters use accent color and bold weight; the selected row keeps a readable +foreground against its accent background. Empty results cannot open a hidden +file. Closing and reopening preserves the session query and starts in navigation. +Entering another directory clears it; refreshing preserves it. A restart starts +with an empty query. + +Mouse motion highlights the row under the cursor without selecting it or reading +a preview. Hover and selection have distinct backgrounds. A click selects the +row and leaves focus in Files. Selecting a file, by keyboard, click, wheel, or +filter change, shows a labelled read-only preview in the main viewer. Enter opens +it through the native Echo text-import path and focuses the editor. A directory +click selects it; Enter enters it. + +Previewing preserves the active buffer, including unsaved edits. Existing buffer +projections supply their current text; other files are read at most 64 KiB and +200 lines. Binary, unavailable, loading, and truncated previews are labelled. +Late results cannot replace the current selection's preview. Closing or leaving +Files restores the existing editor. Previews create no Echo buffers and have no +save action or persistent cache. The projection boundary is recorded in +[component ownership](../../jim-component-ownership.md). + +Search covers currently loaded directory entries. Workspace-wide discovery, +semantic retrieval, and Graft symbols are deferred to +[issue #341](https://github.com/flyingrobots/jedit/issues/341). No SQLite database +or persistent index is created. ## Graft Drawer @@ -67,4 +113,6 @@ replaces the explanation. | [`src/ui/graft-drawer.ts`](../../../src/ui/graft-drawer.ts) | Graft drawer rendering. | | [`src/ui/graft-diagnostics-panel.ts`](../../../src/ui/graft-diagnostics-panel.ts) | Diagnostics panel rendering. | | [`src/ui/why-inline-panel.ts`](../../../src/ui/why-inline-panel.ts) | Inline why explanation rendering. | +| [`src/app/workspace/file-explorer-preview.ts`](../../../src/app/workspace/file-explorer-preview.ts) | Selected-file preview lifecycle and stale-result protection. | +| [`src/adapters/file-preview.ts`](../../../src/adapters/file-preview.ts) | Bounded filesystem preview reads. | | [`src/app/workspace/file-tree.ts`](../../../src/app/workspace/file-tree.ts) | File tree state and entry behavior. | diff --git a/spec/file-explorer-preview.spec.mjs b/spec/file-explorer-preview.spec.mjs new file mode 100644 index 00000000..8e034bb1 --- /dev/null +++ b/spec/file-explorer-preview.spec.mjs @@ -0,0 +1,110 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { importDist, mockRuntime } from './workspace-helpers.mjs'; + +const entries = [ + { kind: 'file', name: 'alpha.txt', path: '/repo/alpha.txt' }, + { kind: 'file', name: 'beta.txt', path: '/repo/beta.txt' }, +]; +const key = (key, flags = {}) => ({ type: 'key', key, ...flags }); +async function setup() { + const { createWorkspaceRuntime } = await importDist('app', 'workspace', 'runtime.js'); + const { BijouI18nAdapter } = await importDist('adapters', 'bijou-i18n-adapter.js'); + const reads = []; + const deps = mockRuntime(); + deps.initialModel.entries = entries; + deps.initialModel.i18n = new BijouI18nAdapter(); + const { availableJeditThemes } = await importDist('ui', 'jedit-themes.js'); + deps.initialModel.jeditTheme = availableJeditThemes()[0]; + deps.filePreviewSource = { loadFilePreview: (path) => { + reads.push(path); + return { kind: 'loaded', lines: [`PREVIEW ${path}`], truncated: false }; + } }; + const runtime = createWorkspaceRuntime(deps); + const model = { ...runtime.init()[0], startupIntroComplete: true }; + return { runtime, model, reads }; +} +function text(surface) { + return Array.from({ length: surface.height }, (_, y) => ( + Array.from({ length: surface.width }, (_, x) => surface.get(x, y).char).join('') + )).join('\n'); +} + +test('selection previews in the main viewer without replacing editor state or opening an Echo buffer', async () => { + const { runtime, model, reads } = await setup(); + const [opened, commands] = runtime.update(key('b', { ctrl: true }), model); + assert.equal(opened.focusPane, 'files'); + assert.equal(opened.textAuthority, model.textAuthority); + assert.ok(commands.length > 0); + const message = await commands.at(-1)(); + const [preview] = runtime.update(message, { ...opened, fileDrawerProgress: 1 }); + assert.match(text(runtime.view(preview)), /Preview.*alpha.txt/); + assert.match(text(runtime.view(preview)), /PREVIEW.*alpha.txt/); + assert.deepEqual(reads, ['/repo/alpha.txt']); + assert.equal(preview.editor, model.editor); + assert.equal(preview.textAuthority, model.textAuthority); + assert.equal(preview.textRequestId, model.textRequestId); + assert.equal(preview.focusPane, 'files'); + const [closed] = runtime.update(key('b', { ctrl: true }), preview); + assert.doesNotMatch(text(runtime.view(closed)), /PREVIEW/); +}); + +test('late preview results cannot paint a different selection and hover never reads a file', async () => { + const { runtime, model, reads } = await setup(); + const [opened, first] = runtime.update(key('b', { ctrl: true }), model); + const [next, second] = runtime.update(key('j'), opened); + const firstMessage = await first.at(-1)(); + const [late] = runtime.update(firstMessage, next); + assert.equal(late.fileExplorerPreview.entry.path, '/repo/beta.txt'); + assert.equal(late.fileExplorerPreview.result, undefined); + const secondMessage = await second.at(-1)(); + const [loaded] = runtime.update(secondMessage, next); + assert.equal(loaded.fileExplorerPreview.result.lines[0], 'PREVIEW /repo/beta.txt'); + const [, hoverCommands] = runtime.update({ type: 'mouse', action: 'move', button: 'none', col: 5, row: 5 }, { + ...loaded, fileDrawerProgress: 1, + }); + assert.deepEqual(hoverCommands, []); + assert.equal(reads.length, 2); +}); + +test('the active unsaved buffer projection is previewed without filesystem reads or mutation', async () => { + const { runtime, model, reads } = await setup(); + const editor = { path: '/repo/alpha.txt', lines: ['UNSAVED CONTENT'], dirty: true }; + const initial = { ...model, editor }; + const [opened] = runtime.update(key('b', { ctrl: true }), initial); + assert.equal(opened.fileExplorerPreview.result.lines[0], 'UNSAVED CONTENT'); + assert.deepEqual(reads, []); + assert.equal(opened.editor, editor); + assert.equal(opened.textAuthority, initial.textAuthority); +}); + +test('an open buffer previews its current reading rather than a stale opening window', async () => { + const { runtime, model, reads } = await setup(); + const cache = { coverage: 'window', startLine: 0, lines: ['CURRENT'], totalLineCount: 500, truncated: false }; + const textAuthority = { kind: 'opened', filePath: '/repo/alpha.txt', cache }; + const initial = { ...model, editor: { path: '/repo/alpha.txt', lines: ['STALE'] }, textAuthority }; + const [opened] = runtime.update(key('b', { ctrl: true }), initial); + assert.deepEqual(opened.fileExplorerPreview.result, { kind: 'loaded', lines: ['CURRENT'], truncated: true }); + assert.deepEqual(reads, []); + const [scrolled, commands] = runtime.update(key('b', { ctrl: true }), { + ...initial, textAuthority: { ...textAuthority, cache: { ...cache, startLine: 40 } }, + }); + assert.equal(scrolled.fileExplorerPreview.result, undefined); + assert.ok(commands.length > 0, 'a reading that misses the head falls back to the file'); +}); + + test('click selection previews without opening and closing rejects its outstanding result', async () => { + const { runtime, model } = await setup(); + const [opened] = runtime.update(key('b', { ctrl: true }), model); + const [selected, commands] = runtime.update({ type: 'mouse', action: 'press', button: 'left', col: 5, row: 6 }, { + ...opened, fileDrawerProgress: 1, + }); + assert.equal(selected.selectedIndex, 1); + assert.equal(selected.focusPane, 'files'); + assert.equal(selected.textRequestId, model.textRequestId); + const message = await commands.at(-1)(); + const [closed] = runtime.update(key('b', { ctrl: true }), selected); + const [late] = runtime.update(message, closed); + assert.equal(late.fileExplorerPreview, undefined); + assert.equal(late.textAuthority, model.textAuthority); +}); diff --git a/spec/file-explorer-search.spec.mjs b/spec/file-explorer-search.spec.mjs new file mode 100644 index 00000000..6e8ef44b --- /dev/null +++ b/spec/file-explorer-search.spec.mjs @@ -0,0 +1,301 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { QUIT } from '@flyingrobots/bijou-tui'; +import { importDist, mockRuntime } from './workspace-helpers.mjs'; + +const ENTRIES = [ + { kind: 'parent', name: '..', path: '/repo' }, + { kind: 'dir', name: 'components', path: '/repo/src/components' }, + { kind: 'file', name: 'alpha.txt', path: '/repo/src/alpha.txt' }, + { kind: 'file', name: 'index.ts', path: '/repo/src/index.ts' }, + { kind: 'file', name: 'render-tree.ts', path: '/repo/src/render-tree.ts' }, + { kind: 'file', name: 'readme.md', path: '/repo/src/readme.md' }, +]; +const key = (key, flags = {}) => ({ type: 'key', key, ...flags }); + +async function setup(options = {}) { + const { createWorkspaceRuntime } = await importDist('app', 'workspace', 'runtime.js'); + const { BijouI18nAdapter } = await importDist('adapters', 'bijou-i18n-adapter.js'); + const { availableJeditThemes } = await importDist('ui', 'jedit-themes.js'); + const deps = mockRuntime(options.deps); + deps.initialModel.entries = options.entries ?? ENTRIES; + deps.initialModel.i18n = new BijouI18nAdapter().withLocale(options.locale ?? 'en'); + deps.initialModel.jeditTheme = availableJeditThemes()[0]; + const runtime = createWorkspaceRuntime(deps); + const initial = { ...runtime.init()[0], cwd: '/repo/src', startupIntroComplete: true }; + const [opened] = runtime.update(key('b', { ctrl: true }), initial); + const focused = options.navigation ? opened : runtime.update(key('/'), opened)[0]; + return { runtime, initial, model: { ...focused, fileDrawerProgress: 1 } }; +} + +function type(runtime, model, input) { + for (const character of input) model = runtime.update(key(character), model)[0]; + return model; +} + +function text(surface) { + return Array.from({ length: surface.height }, (_, y) => ( + Array.from({ length: surface.width }, (_, x) => surface.get(x, y).char).join('') + )).join('\n'); +} + +async function drawer(model, width = 32, height = 12) { + const { renderDrawer } = await importDist('app', 'workspace', 'viewer-drawers.js'); + return renderDrawer('files', model, width, height); +} + +test('slash focuses a sticky search input with a visible caret', async () => { + const { model } = await setup(); + assert.equal(model.focusPane, 'files'); + assert.deepEqual(model.fileSearch, { input: '', cursorIndex: 0 }); + const surface = await drawer(model); + assert.match(text(surface).split('\n')[1], /Search: Filter files/); + assert.equal(surface.get(9, 1).bg, model.jeditTheme.cursor.insert.bg); +}); + +test('typing fuzzy-filters loaded relative paths without mutating the source list', async () => { + const { runtime, model } = await setup(); + const next = type(runtime, model, 'SRC/RNT'); + assert.equal(next.fileSearch.input, 'SRC/RNT'); + assert.equal(next.entries, model.entries); + assert.equal(next.selectedIndex, 4); + const rendered = text(await drawer(next)); + assert.match(rendered, /render-tree\.ts/); + assert.doesNotMatch(rendered, /alpha\.txt|index\.ts|readme\.md|components\/|\.\.\//); +}); + +test('plain shortcut characters belong to the focused search field while Ctrl+C still quits', async () => { + const { runtime, model } = await setup(); + let next = model; + for (const character of 'qjkrh:l` ') { + const [updated, commands] = runtime.update(key(character), next); + assert.deepEqual(commands, []); + assert.equal(updated.quitConfirmOpen, false); + assert.equal(updated.perfVisible, false); + next = updated; + } + assert.equal(next.fileSearch.input, 'qjkrh:l` '); + const [, commands] = runtime.update(key('c', { ctrl: true }), next); + assert.equal(await commands[0](), QUIT); +}); + +test('the query caret edits Unicode text and supports clearing without leaving the directory', async () => { + const { runtime, model } = await setup(); + let next = type(runtime, model, 'a🦀界'); + next = runtime.update(key('left'), next)[0]; + next = runtime.update(key('backspace'), next)[0]; + assert.deepEqual(next.fileSearch, { input: 'a界', cursorIndex: 1 }); + next = runtime.update(key('delete'), next)[0]; + assert.deepEqual(next.fileSearch, { input: 'a', cursorIndex: 1 }); + next = runtime.update(key('home'), next)[0]; + next = type(runtime, next, 'é'); + assert.equal(next.fileSearch.input, 'éa'); + next = runtime.update(key('u', { ctrl: true }), next)[0]; + assert.deepEqual(next.fileSearch, { input: '', cursorIndex: 0 }); + assert.equal(next.cwd, '/repo/src'); + next = runtime.update(key('backspace'), next)[0]; + assert.equal(next.cwd, '/repo/src'); +}); + +test('arrows select visible matches and Enter opens the displayed file, including no-match safety', async () => { + const { runtime, model } = await setup(); + let next = type(runtime, model, '.ts'); + assert.equal(next.selectedIndex, 3); + next = runtime.update(key('down'), next)[0]; + assert.equal(next.selectedIndex, 4); + next = runtime.update(key('escape'), next)[0]; + const [opening, commands] = runtime.update(key('enter'), next); + assert.equal(opening.textAuthority.filePath, '/repo/src/render-tree.ts'); + assert.equal(commands.length, 1); + const empty = type(runtime, model, 'not-a-match'); + assert.match(text(await drawer(empty)), /No matches/); + const [unchanged, none] = runtime.update(key('enter'), empty); + assert.equal(unchanged.textAuthority, empty.textAuthority); + assert.equal(unchanged.selectedIndex, -1); + assert.deepEqual(none, []); +}); + +test('the input stays pinned while the filtered list scrolls and caret text scrolls horizontally', async () => { + const entries = Array.from({ length: 30 }, (_, i) => ({ kind: 'file', name: `entry-${i}.ts`, path: `/repo/src/entry-${i}.ts` })); + const { runtime, model } = await setup({ entries }); + let next = type(runtime, model, 'en'); + const initialHeader = text(await drawer(next, 28, 10)).split('\n')[1]; + for (let i = 0; i < 22; i += 1) next = runtime.update(key('down'), next)[0]; + const rendered = text(await drawer(next, 28, 10)).split('\n'); + assert.equal(rendered[1], initialHeader); + assert.ok(rendered.some((line) => line.includes('› entry-22.ts'))); + next = runtime.update(key('u', { ctrl: true }), next)[0]; + next = type(runtime, next, 'a'.repeat(40) + 'tail'); + assert.match(text(await drawer(next, 28, 10)).split('\n')[1], /tail/); +}); + +test('filtered result clicks and wheel selection use the pinned-header geometry', async () => { + const { runtime, model } = await setup(); + const filtered = type(runtime, model, '.ts'); + const [clicked] = runtime.update({ type: 'mouse', action: 'press', button: 'left', col: 5, row: 6 }, filtered); + assert.equal(clicked.selectedIndex, 4); + assert.equal(clicked.textAuthority, model.textAuthority); + const [wheeled] = runtime.update({ type: 'mouse', action: 'scroll-down', button: 'none', col: 5, row: 8 }, filtered); + assert.equal(wheeled.selectedIndex, 4); + const [inputClick, commands] = runtime.update({ type: 'mouse', action: 'press', button: 'left', col: 10, row: 3 }, { + ...filtered, focusPane: 'editor', + }); + assert.equal(inputClick.focusPane, 'files'); + assert.deepEqual(commands, []); + assert.equal(inputClick.textAuthority, model.textAuthority); +}); + +test('close/reopen retains the query, directory changes clear it, and failed changes retain it', async () => { + const { runtime, model } = await setup({ deps: { fileSystem: { + loadEntries: (cwd) => { if (cwd === '/repo/src/components') throw new Error('unreadable'); return ENTRIES; }, + dirname: () => '/repo', join: (...parts) => parts.join('/'), resolve: (...parts) => parts.join('/'), + describeDirectoryIssue: () => ({ title: 'Directory failed', message: 'unreadable' }), + } } }); + let next = type(runtime, model, 'rnt'); + next = runtime.update(key('b', { ctrl: true }), next)[0]; + assert.equal(next.fileDrawerOpen, false); + next = runtime.update(key('b', { ctrl: true }), next)[0]; + assert.equal(next.focusPane, 'files'); + assert.equal(next.fileSearch.input, 'rnt'); + next = runtime.update(key('up', { alt: true }), next)[0]; + assert.equal(next.cwd, '/repo'); + assert.equal(next.fileSearch.input, ''); + const failed = type(runtime, model, 'components'); + const [after] = runtime.update(key('enter'), runtime.update(key('escape'), failed)[0]); + assert.equal(after.cwd, '/repo/src'); + assert.equal(after.fileSearch.input, 'components'); + assert.equal(after.entries, model.entries); +}); + +test('Settings and pane focus take ownership without changing the search query', async () => { + const { runtime, model } = await setup(); + const filtered = type(runtime, model, 'rnt'); + const settings = runtime.update(key('f2'), filtered)[0]; + assert.equal(settings.settingsOpen, true); + assert.equal(type(runtime, settings, 'q').fileSearch.input, 'rnt'); + const unfocused = { ...filtered, focusPane: 'editor' }; + const surface = await drawer(unfocused); + assert.equal(surface.get(12, 1).bg, model.jeditTheme.surface.currentLine.bg); + assert.equal(unfocused.fileSearch.input, 'rnt'); +}); + +test('refresh keeps the query and no-match pointer actions cannot open a hidden file', async () => { + const { runtime, model } = await setup(); + const filtered = type(runtime, model, 'rnt'); + const refreshed = runtime.update(key('r', { ctrl: true }), filtered)[0]; + assert.deepEqual(refreshed.fileSearch, filtered.fileSearch); + const empty = type(runtime, model, 'zzzzzz'); + for (const action of ['press', 'scroll-down', 'scroll-up']) { + const [next, commands] = runtime.update({ type: 'mouse', action, button: 'left', col: 5, row: 6 }, empty); + assert.equal(next.selectedIndex, -1); + assert.equal(next.textAuthority, empty.textAuthority); + assert.deepEqual(commands, []); + } +}); + +test('localized labels fit narrow fields and wide-character clicks use display columns', async () => { + const { fileSearchCursorAtColumn } = await importDist('ui', 'file-search-input.js'); + const { fileSearchInputOptions } = await importDist('app', 'workspace', 'file-explorer-view.js'); + for (const locale of ['en', 'ja', 'ar', 'me']) { + const { runtime, model } = await setup({ locale }); + const next = type(runtime, model, '界x'); + const options = fileSearchInputOptions(next, 28); + const surface = await drawer(next, 30, 8); + assert.match(text(surface).split('\n')[1], /界x/); + assert.ok(!options.label.includes('fileExplorer.')); + const { visibleLineLength } = await importDist('ui', 'fit-line.js'); + const prefix = visibleLineLength(`${options.label}: `); + assert.equal(fileSearchCursorAtColumn(options, prefix + 2), 1); + assert.equal(fileSearchCursorAtColumn(options, prefix + 3), 2); + } +}); + +test('the input has a distinct background and only fuzzy-matched letters receive the accent', async () => { + const { runtime, model } = await setup({ entries: [ENTRIES[4], { ...ENTRIES[4], name: 'render-test.ts', path: '/repo/src/render-test.ts' }] }); + const filtered = type(runtime, model, 'rnt'); + const surface = await drawer(filtered); + assert.notEqual(surface.get(2, 1).bg, surface.get(2, 3).bg); + assert.equal(surface.get(2, 1).bg, model.jeditTheme.surface.currentLine.bg); + for (const x of [3, 5, 10]) { + assert.equal(surface.get(x, 3).fg, surface.get(4, 3).fg); + assert.equal(surface.get(x, 4).fg, model.jeditTheme.chrome.activeEdge.fg); + assert.ok(surface.get(x, 3).modifiers.includes('bold')); + } + assert.equal(surface.get(4, 4).fg, model.jeditTheme.surface.drawer.fg); + const cleared = runtime.update(key('u', { ctrl: true }), filtered)[0]; + const plain = await drawer(cleared); + assert.equal(plain.get(3, 4).fg, model.jeditTheme.surface.drawer.fg); +}); + +test('the input background remains distinct across built-in dark and light themes', async () => { + const { availableJeditThemes, oppositeJeditTheme } = await importDist('ui', 'jedit-themes.js'); + const { model } = await setup(); + for (const theme of availableJeditThemes().flatMap((theme) => [theme, oppositeJeditTheme(theme)])) { + const surface = await drawer({ ...model, jeditTheme: theme }); + assert.notEqual(surface.get(2, 1).bg, surface.get(2, 3).bg, theme.name); + } +}); + + test('Files opens in navigation mode, slash edits, and Escape returns to navigation before closing', async () => { + const { runtime, model } = await setup({ navigation: true }); + assert.equal(model.fileSearchEditing, false); + let next = runtime.update(key('j'), model)[0]; + assert.equal(next.selectedIndex, 1); + assert.equal(next.fileSearch.input, ''); + next = runtime.update(key('k'), next)[0]; + assert.equal(next.selectedIndex, 0); + next = runtime.update(key('/'), next)[0]; + assert.equal(next.fileSearchEditing, true); + next = type(runtime, next, '.ts'); + next = runtime.update(key('escape'), next)[0]; + assert.equal(next.fileDrawerOpen, true); + assert.equal(next.fileSearchEditing, false); + assert.equal(next.fileSearch.input, '.ts'); + next = runtime.update(key('j'), next)[0]; + assert.equal(next.selectedIndex, 4); + next = runtime.update(key('escape'), next)[0]; + assert.equal(next.fileDrawerOpen, false); +}); + + test('mouse hover is separate from selection and clicking selects without opening', async () => { + const { runtime, model } = await setup({ navigation: true }); + const [hover, commands] = runtime.update({ type: 'mouse', action: 'move', button: 'none', col: 5, row: 7 }, model); + assert.equal(hover.fileHoveredIndex, 2); + assert.equal(hover.selectedIndex, 0); + assert.deepEqual(commands, []); + const surface = await drawer(hover); + assert.notEqual(surface.get(2, 3).bg, surface.get(2, 5).bg); + const [selected] = runtime.update({ type: 'mouse', action: 'press', button: 'left', col: 5, row: 7 }, hover); + assert.equal(selected.selectedIndex, 2); + assert.equal(selected.focusPane, 'files'); + assert.equal(selected.textAuthority, model.textAuthority); + const [outside] = runtime.update({ type: 'mouse', action: 'move', button: 'none', col: 80, row: 7 }, selected); + assert.equal(outside.fileHoveredIndex, undefined); +}); + + test('portable paths from the filesystem boundary exclude drive and workspace text from filtering', async () => { + const { portableFileSearchPath } = await importDist('adapters', 'filesystem.js'); + const { runtime, model } = await setup({ entries: [{ + kind: 'file', name: 'index.ts', path: 'C:\\repo\\src\\index.ts', + searchPath: portableFileSearchPath('C:\\repo\\src\\index.ts'), + }] }); + const initial = { ...model, workspaceRoot: 'C:\\repo', workspaceSearchRoot: portableFileSearchPath('C:\\repo') }; + assert.equal(type(runtime, initial, 'src/index').selectedIndex, 0); + assert.equal(type(runtime, initial, 'repo').selectedIndex, -1); + const above = { ...initial, entries: [{ kind: 'file', name: 'notes.md', path: 'C:\\repo\\notes.md', searchPath: portableFileSearchPath('C:\\repo\\notes.md') }] }; + assert.equal(type(runtime, { ...above, workspaceSearchRoot: portableFileSearchPath('C:\\repo\\src') }, 'repo').selectedIndex, -1); + assert.equal(type(runtime, above, 'notes').selectedIndex, 0); +}); + + test('selected rows remain readable and distinct from hover across dark and light themes', async () => { + const { availableJeditThemes, oppositeJeditTheme } = await importDist('ui', 'jedit-themes.js'); + const { contrastRatio } = await importDist('ui', 'theme-contrast.js'); + const { model } = await setup({ navigation: true }); + for (const theme of availableJeditThemes().flatMap((theme) => [theme, oppositeJeditTheme(theme)])) { + const surface = await drawer({ ...model, jeditTheme: theme, selectedIndex: 1, fileHoveredIndex: 2 }); + const selected = surface.get(3, 4); + const hovered = surface.get(3, 5); + assert.notEqual(selected.bg, hovered.bg, theme.name); + assert.ok(contrastRatio(selected.fgRGB, selected.bgRGB) >= 4.45, theme.name); + } +}); diff --git a/spec/file-preview-source.spec.mjs b/spec/file-preview-source.spec.mjs new file mode 100644 index 00000000..646f26a0 --- /dev/null +++ b/spec/file-preview-source.spec.mjs @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { mkdtempSync, writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { tmpdir } from 'node:os'; +import { spawnSync } from 'node:child_process'; +import { importDist } from './dist-helpers.mjs'; + +test('filesystem previews bound bytes and lines and report binary or unavailable files', async () => { + const { filePreviewSource } = await importDist('adapters', 'file-preview.js'); + const dir = mkdtempSync(join(tmpdir(), 'jedit-preview-spec-')); + const large = join(dir, 'large.txt'); + writeFileSync(large, '界'.repeat(40000)); + const truncated = filePreviewSource.loadFilePreview(large); + assert.equal(truncated.kind, 'loaded'); + assert.equal(truncated.truncated, true); + assert.ok(Buffer.byteLength(truncated.lines.join('\n')) <= 65536); + assert.ok(!truncated.lines.join('').includes('�')); + const many = join(dir, 'many.txt'); + writeFileSync(many, 'line\n'.repeat(250)); + const lines = filePreviewSource.loadFilePreview(many); + assert.equal(lines.lines.length, 200); + assert.equal(lines.truncated, true); + const binary = join(dir, 'binary'); + writeFileSync(binary, Buffer.from([0, 1, 2])); + assert.equal(filePreviewSource.loadFilePreview(binary).kind, 'binary'); + assert.equal(filePreviewSource.loadFilePreview(join(dir, 'missing')).kind, 'unavailable'); + assert.equal(filePreviewSource.loadFilePreview(dir).kind, 'unavailable'); +}); + + test('a named pipe cannot block the preview reader', async (context) => { + const { filePreviewSource } = await importDist('adapters', 'file-preview.js'); + const dir = mkdtempSync(join(tmpdir(), 'jedit-preview-pipe-')); + const pipe = join(dir, 'pipe'); + const created = spawnSync('mkfifo', [pipe]); + if (created.error?.code === 'ENOENT') { context.skip('mkfifo unavailable'); return; } + assert.equal(created.status, 0); + const script = `import {filePreviewSource} from ${JSON.stringify(new URL('../dist/adapters/file-preview.js', import.meta.url).href)};` + + `process.stdout.write(JSON.stringify(filePreviewSource.loadFilePreview(${JSON.stringify(pipe)})));`; + const result = spawnSync(process.execPath, ['--input-type=module', '-e', script], { timeout: 1000, encoding: 'utf8' }); + assert.equal(result.error, undefined); + assert.equal(JSON.parse(result.stdout).kind, 'unavailable'); + assert.equal(filePreviewSource.loadFilePreview(dir).kind, 'unavailable'); +}); diff --git a/spec/terminal-input-stream.spec.mjs b/spec/terminal-input-stream.spec.mjs new file mode 100644 index 00000000..14373fe0 --- /dev/null +++ b/spec/terminal-input-stream.spec.mjs @@ -0,0 +1,36 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { setTimeout as delay } from 'node:timers/promises'; +import { importDist } from './workspace-helpers.mjs'; + +const ESCAPE_SETTLE_MS = 60; + +async function stream() { + const { terminalInputStream } = await importDist('adapters', 'terminal-input-stream.js'); + const tokens = []; + return { tokens, input: terminalInputStream((token) => tokens.push(token)) }; +} + +test('complete escape sequences and printable keys are split into tokens', async () => { + const { tokens, input } = await stream(); + input.accept('a\u001b[Ab'); + assert.deepEqual(tokens, ['a', '\u001b[A', 'b']); + input.dispose(); +}); + +test('an unfinished CSI prefix is flushed after the escape delay', async () => { + const { tokens, input } = await stream(); + input.accept('\u001b['); + await delay(ESCAPE_SETTLE_MS); + input.accept('\u0003'); + assert.deepEqual(tokens, ['\u001b[', '\u0003']); + input.dispose(); +}); + +test('control keys inside an unfinished CSI prefix are delivered on their own', async () => { + const { tokens, input } = await stream(); + input.accept('\u001b['); + input.accept('\u0003'); + assert.deepEqual(tokens, ['\u001b[', '\u0003']); + input.dispose(); +}); diff --git a/spec/workspace-drawer-scroll.spec.mjs b/spec/workspace-drawer-scroll.spec.mjs index 48b0c3ec..86fd8de8 100644 --- a/spec/workspace-drawer-scroll.spec.mjs +++ b/spec/workspace-drawer-scroll.spec.mjs @@ -80,10 +80,10 @@ test("a click maps through the scroll offset to the right entry", async () => { rows: model.rows, footerVisible: model.footerVisible, }); - const listHeight = bodyHeight - (viewport.DRAWER_INNER_PAD * 2); + const listHeight = viewport.fileSearchListHeight(bodyHeight); const offset = viewport.listScrollOffset(60, entries.length, listHeight); const firstRow = - viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.DRAWER_INNER_PAD; + viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.FILE_SEARCH_LIST_TOP; const [next] = mouse.updateFromMouse( { @@ -103,7 +103,7 @@ test("a click maps through the scroll offset to the right entry", async () => { assert.equal(next.selectedIndex, offset); }); -test("clicking a file opens it, not just selects it", async () => { +test("clicking a file selects it and leaves opening to Enter", async () => { const [mouse, viewport, panelFocus, titleScreen] = await Promise.all([ importDist("app", "workspace", "mouse.js"), importDist("app", "workspace", "viewport.js"), @@ -126,9 +126,9 @@ test("clicking a file opens it, not just selects it", async () => { columns: 100, rows: 30, }); - const row = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.DRAWER_INNER_PAD + 1; + const row = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.FILE_SEARCH_LIST_TOP + 1; - mouse.updateFromMouse( + const [selected] = mouse.updateFromMouse( { type: "mouse", button: "left", action: "press", col: 2, row, shift: false, alt: false, ctrl: false }, model, { highlight: () => undefined }, @@ -141,5 +141,6 @@ test("clicking a file opens it, not just selects it", async () => { }, ); - assert.deepEqual(opened, ["/w/beta.txt"]); + assert.equal(selected.selectedIndex, 1); + assert.deepEqual(opened, []); }); diff --git a/spec/workspace-footer.spec.mjs b/spec/workspace-footer.spec.mjs index c744ce15..56828db1 100644 --- a/spec/workspace-footer.spec.mjs +++ b/spec/workspace-footer.spec.mjs @@ -263,7 +263,7 @@ test('workspace footer shows file drawer controls and the selected file path', a graftSelection: undefined, }), [ - 'FILES [j/k move · enter open · backspace up · ctrl+b close · tab focus]', + 'FILES [j/k move · slash filter · enter open · ctrl+b close · tab focus]', '/repo/notes/very-long-file-name.md', ], ); @@ -380,7 +380,7 @@ test('workspace footer obtains command hints from i18n', async () => { }); assert.equal(primary.includes(''), true); - assert.equal(primary.includes(''), true); + assert.equal(primary.includes(''), true); assert.equal(primary.includes(''), true); assert.equal(requestedKeys.includes('footer.hints.enter_open'), true); assert.equal(requestedKeys.includes('footer.hints.ctrl_b_close'), true); diff --git a/spec/workspace-list-navigation.spec.mjs b/spec/workspace-list-navigation.spec.mjs index b45d3cfd..bd15ec46 100644 --- a/spec/workspace-list-navigation.spec.mjs +++ b/spec/workspace-list-navigation.spec.mjs @@ -80,7 +80,7 @@ test("the file explorer selection wraps from the last entry to the first", async const model = await fileDrawerModel({ selectedIndex: LAST_INDEX }); const [next] = keyBindings.updateFromKey( - { key: "j" }, + { key: "down" }, model, mockKeyBindingContext(), ); @@ -93,7 +93,7 @@ test("the file explorer selection wraps from the first entry to the last", async const model = await fileDrawerModel({ selectedIndex: FIRST_INDEX }); const [next] = keyBindings.updateFromKey( - { key: "k" }, + { key: "up" }, model, mockKeyBindingContext(), ); @@ -106,7 +106,7 @@ test("an empty file explorer stays put instead of wrapping onto nothing", async const model = await fileDrawerModel({ entries: [], selectedIndex: 0 }); const [next] = keyBindings.updateFromKey( - { key: "j" }, + { key: "down" }, model, mockKeyBindingContext(), ); @@ -180,7 +180,7 @@ test("clicking a file explorer row selects that entry", async () => { importDist("app", "workspace", "viewport.js"), ]); const model = await fileDrawerModel({ columns: 100, rows: 30, fileDrawerProgress: 1 }); - const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.DRAWER_INNER_PAD + 2; + const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.FILE_SEARCH_LIST_TOP + 2; const [next] = mouse.updateFromMouse( { type: "mouse", button: "left", action: "press", col: 2, row: clickRow, shift: false, alt: false, ctrl: false }, @@ -197,7 +197,7 @@ test("clicking outside the file explorer leaves the selection alone", async () = importDist("app", "workspace", "viewport.js"), ]); const model = await fileDrawerModel({ columns: 100, rows: 30, fileDrawerProgress: 1, selectedIndex: 1 }); - const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.DRAWER_INNER_PAD + 2; + const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.FILE_SEARCH_LIST_TOP + 2; const [next] = mouse.updateFromMouse( { type: "mouse", button: "left", action: "press", col: 95, row: clickRow, shift: false, alt: false, ctrl: false }, @@ -214,7 +214,7 @@ test("clicking past the last entry leaves the selection alone", async () => { importDist("app", "workspace", "viewport.js"), ]); const model = await fileDrawerModel({ columns: 100, rows: 30, fileDrawerProgress: 1, selectedIndex: 1 }); - const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.DRAWER_INNER_PAD + 20; + const clickRow = viewport.WORKSPACE_BODY_TOP_OFFSET + viewport.FILE_SEARCH_LIST_TOP + 20; const [next] = mouse.updateFromMouse( { type: "mouse", button: "left", action: "press", col: 2, row: clickRow, shift: false, alt: false, ctrl: false }, diff --git a/spec/workspace-terminal-input.spec.mjs b/spec/workspace-terminal-input.spec.mjs new file mode 100644 index 00000000..533756a5 --- /dev/null +++ b/spec/workspace-terminal-input.spec.mjs @@ -0,0 +1,43 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { createEventBus } from '@flyingrobots/bijou-tui'; +import { importDist } from './dist-helpers.mjs'; + + test('the production input boundary emits Unicode, modified arrows, and hover through the Bijou event bus', async () => { + const { workspaceTerminalIO } = await importDist('adapters', 'workspace-terminal-io.js'); + let input; + let disposed = false; + const rawIO = { rawInput: (handler) => { + input = handler; + return { dispose: () => { disposed = true; } }; + }, onResize: () => ({ dispose() {} }) }; + const bus = createEventBus(); + const received = []; + bus.on((msg) => received.push(msg)); + const connection = bus.connectIO(workspaceTerminalIO(rawIO), { mouse: true }); + input('/é界🦀'); + input('\x1b[1;3A'); + input('\x1b[<35;6;8M'); + assert.deepEqual(received.map((msg) => msg.key ?? msg.action), ['/', 'é', '界', '🦀', 'up', 'move']); + assert.equal(received[4].alt, true); + assert.equal(received[5].col, 5); + assert.equal(received[5].row, 7); + connection.dispose(); + assert.equal(disposed, true); +}); + + test('split escape sequences stay intact, ordinary keys preserve modifiers, and lone Escape is delivered', async () => { + const { workspaceTerminalIO } = await importDist('adapters', 'workspace-terminal-io.js'); + let input; + const rawIO = { rawInput: (handler) => { input = handler; return { dispose() {} }; }, onResize: () => ({ dispose() {} }) }; + const bus = createEventBus(); + const received = []; + bus.on((msg) => received.push(msg)); + const connection = bus.connectIO(workspaceTerminalIO(rawIO), { mouse: true }); + input('\x1b[1;'); input('3A'); input('J\x02'); input('\x1b'); + await new Promise((resolve) => setTimeout(resolve, 40)); + assert.deepEqual(received.map((msg) => msg.key), ['up', 'j', 'b', 'escape']); + assert.equal(received[1].shift, true); + assert.equal(received[2].ctrl, true); + connection.dispose(); +}); diff --git a/src/adapters/file-preview.ts b/src/adapters/file-preview.ts new file mode 100644 index 00000000..97c104df --- /dev/null +++ b/src/adapters/file-preview.ts @@ -0,0 +1,30 @@ +import { constants, closeSync, fstatSync, openSync, readSync } from 'node:fs'; +import { normalizeLines } from '../app/editor-lines.js'; +import { FilePreviewKinds, FILE_PREVIEW_MAX_BYTES, FILE_PREVIEW_MAX_LINES, type FilePreviewSource } from '../ports/file-preview.js'; + +const UTF8_ENCODING = 'utf-8'; +const NULL_BYTE = 0; + +export const filePreviewSource: FilePreviewSource = { + loadFilePreview(path) { + let descriptor: number | undefined; + try { + descriptor = openSync(path, constants.O_RDONLY | constants.O_NONBLOCK); + const stat = fstatSync(descriptor); + if (!stat.isFile()) return { kind: FilePreviewKinds.Unavailable }; + const bytes = Buffer.alloc(FILE_PREVIEW_MAX_BYTES); + const read = readSync(descriptor, bytes, 0, bytes.length, 0); + const content = bytes.subarray(0, read); + if (content.includes(NULL_BYTE)) return { kind: FilePreviewKinds.Binary }; + const lines = normalizeLines(new TextDecoder(UTF8_ENCODING).decode(content, { stream: read < stat.size })); + return { + kind: FilePreviewKinds.Loaded, lines: lines.slice(0, FILE_PREVIEW_MAX_LINES), + truncated: read < stat.size || lines.length > FILE_PREVIEW_MAX_LINES, + }; + } catch { + return { kind: FilePreviewKinds.Unavailable }; + } finally { + if (descriptor != null) closeSync(descriptor); + } + }, +}; diff --git a/src/adapters/filesystem.ts b/src/adapters/filesystem.ts index ba0a2f64..96ca61d1 100644 --- a/src/adapters/filesystem.ts +++ b/src/adapters/filesystem.ts @@ -52,6 +52,7 @@ export function loadEntries(cwd: string): readonly FileEntry[] { kind: 'parent', name: '..', path: parent, + searchPath: portableFileSearchPath(parent), }); } @@ -60,6 +61,7 @@ export function loadEntries(cwd: string): readonly FileEntry[] { kind: entry.isDirectory() ? 'dir' : 'file', name: entry.name, path: join(cwd, entry.name), + searchPath: portableFileSearchPath(join(cwd, entry.name)), })) .sort(compareEntries); @@ -129,3 +131,7 @@ function formatUnknownFilesystemError(cause: Throwable): string { ? cause.message : String(cause); } + +export function portableFileSearchPath(path: string): string { + return path.replaceAll('\\', '/'); +} diff --git a/src/adapters/terminal-input-stream.ts b/src/adapters/terminal-input-stream.ts new file mode 100644 index 00000000..006ad8df --- /dev/null +++ b/src/adapters/terminal-input-stream.ts @@ -0,0 +1,54 @@ +const ESCAPE = '\u001b'; +const CSI_START = `${ESCAPE}[`; +const SS3_START = `${ESCAPE}O`; +const CSI_FINAL = /[@-~]/u; +const ESCAPE_DELAY_MS = 25; +const FIRST_SEQUENCE_CODE = 0x20; +const LAST_SEQUENCE_CODE = 0x7e; + +export function terminalInputStream(emit: (token: string) => void) { + let pending = ''; + let timer: ReturnType | undefined; + const clearTimer = () => { if (timer != null) clearTimeout(timer); }; + return { + accept(raw: string) { + clearTimer(); + pending += raw; + while (pending.length > 0) { + const length = terminalTokenLength(pending); + if (length == null) break; + emit(pending.slice(0, length)); + pending = pending.slice(length); + } + // A bare Escape or an unfinished sequence (e.g. Alt+[ sending ESC[) is + // flushed after the delay so it can never hold later input hostage. + if (pending.length > 0) { + timer = setTimeout(() => { const flushed = pending; pending = ''; emit(flushed); }, ESCAPE_DELAY_MS); + } + }, + dispose() { clearTimer(); pending = ''; }, + }; +} + +function terminalTokenLength(raw: string): number | undefined { + if (raw === ESCAPE) return undefined; + if (raw.startsWith(CSI_START)) return csiTokenLength(raw); + if (raw.startsWith(SS3_START)) return raw.length >= 3 ? 3 : undefined; + return Array.from(raw)[0]?.length; +} + +function csiTokenLength(raw: string): number | undefined { + for (let index = CSI_START.length; index < raw.length; index += 1) { + const character = raw[index] ?? ''; + // A control byte (Ctrl+C, Escape) cannot belong to a CSI sequence: end the + // unfinished prefix there so the control key is delivered on its own. + if (!isSequenceByte(character)) return index; + if (CSI_FINAL.test(character)) return index + 1; + } + return undefined; +} + +function isSequenceByte(character: string): boolean { + const code = character.charCodeAt(0); + return code >= FIRST_SEQUENCE_CODE && code <= LAST_SEQUENCE_CODE; +} diff --git a/src/adapters/workspace-app.ts b/src/adapters/workspace-app.ts index e1a90503..a84a951f 100644 --- a/src/adapters/workspace-app.ts +++ b/src/adapters/workspace-app.ts @@ -1,3 +1,4 @@ +import { filePreviewSource } from './file-preview.js'; import { FileSystemPortAdapter } from './filesystem.js'; import { createWorkspaceRuntime } from '../app/workspace/runtime.js'; import { createRaytracerProfilerPort } from './raytracer-profiler.js'; @@ -16,8 +17,6 @@ import { createWorkspaceNotificationTickCmd, createWorkspaceTimeTickCmd, } from './workspace-animation-commands.js'; -import { openWorkspaceFileEntry } from '../app/workspace/file-tree.js'; -import type { WorkspaceModel } from '../app/workspace/model.js'; export interface WorkspaceAppOptions { initialColumns: number; @@ -54,6 +53,7 @@ function workspaceRuntimeDependencies( const graftSession = createGraftSessionPort(); const sourceHighlighter = createGraftSourceHighlighter(); return { + filePreviewSource, initialColumns: options.initialColumns, initialRows: options.initialRows, initialWorkingDirectory: options.initialWorkingDirectory, @@ -67,16 +67,6 @@ function workspaceRuntimeDependencies( profileOnStartup: options.profileEnabled ?? false, initialModel: options.seed ?? createInitialModelSnapshot(nowMs(), options.initialWorkingDirectory), nowMs, - openEntry: ( - model: WorkspaceModel, - entry: NonNullable, - ) => openWorkspaceFileEntry(model, entry, nowMs, { - fileSystem: FileSystemPortAdapter, - editorFile, - graftSession, - sourceHighlighter, - productionTextSession: productionText.productionTextSession, - }), createTimeTickCmd: createWorkspaceTimeTickCmd, createNotificationTickCmd: createWorkspaceNotificationTickCmd, createDrawerAnimationCmd: createWorkspaceDrawerAnimationCmd, diff --git a/src/adapters/workspace-initial-model-snapshot.ts b/src/adapters/workspace-initial-model-snapshot.ts index 251d7fed..3d5ec6d0 100644 --- a/src/adapters/workspace-initial-model-snapshot.ts +++ b/src/adapters/workspace-initial-model-snapshot.ts @@ -4,7 +4,7 @@ import { JEDIT_THEME_ENV, resolveInitialJeditTheme, } from "../ui/jedit-themes.js"; -import { loadEntries } from "./filesystem.js"; +import { loadEntries, portableFileSearchPath } from "./filesystem.js"; export function createInitialModelSnapshot( nowMs: number, @@ -12,6 +12,7 @@ export function createInitialModelSnapshot( ): WorkspaceInitialModelSnapshot { return { entries: loadEntries(cwd), + searchRoot: portableFileSearchPath(cwd), jeditTheme: resolveInitialJeditTheme(process.env[JEDIT_THEME_ENV]), i18n: new BijouI18nAdapter(), nowMs, diff --git a/src/adapters/workspace-terminal-io.ts b/src/adapters/workspace-terminal-io.ts new file mode 100644 index 00000000..294d7f5d --- /dev/null +++ b/src/adapters/workspace-terminal-io.ts @@ -0,0 +1,46 @@ +import type { IOPort } from '@flyingrobots/bijou'; +import type { KeyMsg } from '@flyingrobots/bijou-tui'; +import { terminalInputStream } from './terminal-input-stream.js'; + +const MODIFIED_ARROW = /^\u001b\[1;(\d+)([ABCDHF])$/u; +const ARROWS: Readonly> = { A: 'up', B: 'down', C: 'right', D: 'left', H: 'home', F: 'end' }; +const NON_ASCII_PRINTABLE = /^[^\x00-\x7f\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]$/u; +const MODIFIER_BASE = 1; +const SHIFT_BIT = 1; +const ALT_BIT = 2; +const CTRL_BIT = 4; + +// Bijou's raw parser handles ASCII, mouse packets and ordinary escape keys. +// Its onData host-message port carries decoded keys that it cannot express. +export function workspaceTerminalIO(io: IOPort): IOPort { + const subscribers = new Set<(message: KeyMsg) => void>(); + return { + ...io, + rawInput(callback) { + const stream = terminalInputStream((raw) => { + const message = extendedTerminalKey(raw); + if (message == null) callback(raw); + else for (const subscriber of subscribers) subscriber(message); + }); + const handle = io.rawInput(stream.accept); + return { dispose() { stream.dispose(); handle.dispose(); } }; + }, + onData(callback) { + const subscriber = (message: KeyMsg) => callback(message); + subscribers.add(subscriber); + const handle = io.onData?.(callback); + return { dispose() { subscribers.delete(subscriber); handle?.dispose(); } }; + }, + }; +} + +function extendedTerminalKey(raw: string): KeyMsg | undefined { + if (NON_ASCII_PRINTABLE.test(raw)) return { type: 'key', key: raw, ctrl: false, alt: false, shift: false }; + const match = MODIFIED_ARROW.exec(raw); + if (match == null) return undefined; + const bits = Number(match[1]) - MODIFIER_BASE; + return { + type: 'key', key: ARROWS[match[2] ?? ''] ?? '', + shift: (bits & SHIFT_BIT) !== 0, alt: (bits & ALT_BIT) !== 0, ctrl: (bits & CTRL_BIT) !== 0, + }; +} diff --git a/src/app/workspace/drawer.ts b/src/app/workspace/drawer.ts index f62b5a69..f8553bb4 100644 --- a/src/app/workspace/drawer.ts +++ b/src/app/workspace/drawer.ts @@ -46,6 +46,7 @@ function openFileDrawer( const next = withFocusPane({ ...model, fileDrawerOpen: true, + fileSearchEditing: false, }, FocusPanes.Files); if (model.fileDrawerOpen) { @@ -77,7 +78,7 @@ export function closeDrawer( createDrawerAnimationCmd: CreateDrawerAnimationCmd, ): [WorkspaceModel, Cmd[]] { const next = kind === DrawerKinds.Files - ? { ...model, fileDrawerOpen: false } + ? { ...model, fileDrawerOpen: false, fileSearchEditing: false, fileHoveredIndex: undefined } : { ...model, graftDrawerOpen: false }; const focusState: Omit = { diff --git a/src/app/workspace/file-explorer-preview-view.ts b/src/app/workspace/file-explorer-preview-view.ts new file mode 100644 index 00000000..89debbd0 --- /dev/null +++ b/src/app/workspace/file-explorer-preview-view.ts @@ -0,0 +1,46 @@ +import { createSurface, stringToSurface, type Surface } from '@flyingrobots/bijou'; +import { FilePreviewKinds, type FilePreviewResult } from '../../ports/file-preview.js'; +import { fitBlock } from '../../ui/workspace-render.js'; +import { fileExplorerHasFocus, selectedFileSearchEntry } from './file-search.js'; +import { FileEntryKinds } from '../../ports/file-system.js'; +import type { WorkspaceModel } from './model.js'; +import { applyBackground, fillSurface } from './surface-fill.js'; + +const PREVIEW_PAD = 1; +const PAD_MULTIPLIER = 2; +const PREVIEW_BODY_TOP = 3; +const PREVIEW_LABEL = 'fileExplorer.preview_label'; +const PREVIEW_LOADING = 'fileExplorer.preview_loading'; +const PREVIEW_BINARY = 'fileExplorer.preview_binary'; +const PREVIEW_UNAVAILABLE = 'fileExplorer.preview_unavailable'; +const PREVIEW_TRUNCATED = 'fileExplorer.preview_truncated'; +const TAB_SPACES = ' '; +const UNSAFE_CONTROLS = /[\p{Cc}\p{Cf}]/gu; + +export function renderFileExplorerPreview(model: WorkspaceModel, width: number, height: number): Surface | undefined { + if (!fileExplorerHasFocus(model)) return undefined; + const entry = selectedFileSearchEntry(model); + if (entry?.kind !== FileEntryKinds.File) return undefined; + const surface = createSurface(width, height); + fillSurface(surface, model.jeditTheme.surface.workspace); + const innerWidth = Math.max(0, width - PREVIEW_PAD * PAD_MULTIPLIER); + const header = `${model.i18n.t(PREVIEW_LABEL)} · ${entry.name}`; + const label = stringToSurface(fitBlock(header, innerWidth, 1), innerWidth, 1); + applyBackground(label, model.jeditTheme.surface.header); + surface.blit(label, PREVIEW_PAD, PREVIEW_PAD); + const result = model.fileExplorerPreview?.entry === entry ? model.fileExplorerPreview.result : undefined; + const lines = previewLines(model, result); + const bodyHeight = Math.max(0, height - PREVIEW_BODY_TOP - PREVIEW_PAD); + const body = stringToSurface(fitBlock(lines.join('\n'), innerWidth, bodyHeight), innerWidth, bodyHeight); + applyBackground(body, model.jeditTheme.surface.workspace); + surface.blit(body, PREVIEW_PAD, PREVIEW_BODY_TOP); + return surface; +} + +function previewLines(model: WorkspaceModel, result: FilePreviewResult | undefined): readonly string[] { + if (result == null) return [model.i18n.t(PREVIEW_LOADING)]; + if (result.kind === FilePreviewKinds.Binary) return [model.i18n.t(PREVIEW_BINARY)]; + if (result.kind === FilePreviewKinds.Unavailable) return [model.i18n.t(PREVIEW_UNAVAILABLE)]; + const lines = result.lines.map((line) => line.replaceAll('\t', TAB_SPACES).replace(UNSAFE_CONTROLS, '')); + return result.truncated ? [model.i18n.t(PREVIEW_TRUNCATED), ...lines] : lines; +} diff --git a/src/app/workspace/file-explorer-preview.ts b/src/app/workspace/file-explorer-preview.ts new file mode 100644 index 00000000..9c6c622c --- /dev/null +++ b/src/app/workspace/file-explorer-preview.ts @@ -0,0 +1,78 @@ +import type { Cmd } from '@flyingrobots/bijou-tui'; +import { FilePreviewKinds, FILE_PREVIEW_MAX_LINES, type FilePreviewSource, type FilePreviewResult } from '../../ports/file-preview.js'; +import { FileEntryKinds, type FileEntry } from '../../ports/file-system.js'; +import { fileExplorerHasFocus, selectedFileSearchEntry } from './file-search.js'; +import { findWorkspaceBufferRecordByPath } from './workspace-buffer-registry.js'; +import { isWorkspaceTextAuthorityOpened } from './workspace-text-authority.js'; +import type { WorkspaceTextReadingCache } from './workspace-text-reading-cache.js'; +import { WorkspaceMessageTypes, type WorkspaceMsg } from './msg.js'; +import type { WorkspaceModel } from './model.js'; + +export interface FileExplorerPreview { + readonly requestId: number; + readonly entry: FileEntry; + readonly result?: FilePreviewResult; +} + +export interface FileExplorerPreviewMessage extends FileExplorerPreview { + readonly result: FilePreviewResult; +} + +type Result = [WorkspaceModel, Cmd[]]; +const FIRST_LINE = 0; +type LoadedFilePreview = Extract; + +export function synchronizeFileExplorerPreview(result: Result, source: FilePreviewSource | undefined): Result { + const [model, commands] = result; + const entry = fileExplorerHasFocus(model) ? selectedFileSearchEntry(model) : undefined; + if (entry?.kind !== FileEntryKinds.File) return clearFileExplorerPreview(result); + if (model.fileExplorerPreview?.entry === entry || source == null) return result; + const requestId = (model.fileExplorerPreviewRequestId ?? 0) + 1; + const preview = { requestId, entry, result: bufferPreview(model, entry.path) }; + const next = { ...model, fileExplorerPreviewRequestId: requestId, fileExplorerPreview: preview }; + return preview.result != null ? [next, commands] : [next, [...commands, loadPreviewCmd(preview, source)]]; +} + +export function applyFileExplorerPreview(model: WorkspaceModel, msg: FileExplorerPreviewMessage): WorkspaceModel { + return model.fileExplorerPreview?.requestId === msg.requestId && + model.fileExplorerPreview.entry === msg.entry && fileExplorerHasFocus(model) + ? { ...model, fileExplorerPreview: msg } + : model; +} + +function clearFileExplorerPreview(result: Result): Result { + return result[0].fileExplorerPreview == null + ? result + : [{ ...result[0], fileExplorerPreview: undefined }, result[1]]; +} + +// Open buffers preview their current text-authority reading, not the editor +// projection, which can be a stale opening window. A reading that does not start +// at the first line cannot show the file's head, so the file is read instead. +function bufferPreview(model: WorkspaceModel, path: string): FilePreviewResult | undefined { + const authority = isWorkspaceTextAuthorityOpened(model.textAuthority) && model.textAuthority.filePath === path + ? model.textAuthority + : findWorkspaceBufferRecordByPath(model, path)?.textAuthority; + if (authority != null) return readingPreview(authority.cache); + return model.editor?.path === path ? linesPreview(model.editor.lines, model.editor.lines.length) : undefined; +} + +function readingPreview(cache: WorkspaceTextReadingCache | undefined): FilePreviewResult | undefined { + if (cache == null || cache.startLine !== FIRST_LINE) return undefined; + const preview = linesPreview(cache.lines, cache.totalLineCount); + return cache.truncated ? { ...preview, truncated: true } : preview; +} + +function linesPreview(lines: readonly string[], totalLineCount: number): LoadedFilePreview { + const shown = lines.slice(0, FILE_PREVIEW_MAX_LINES); + return { kind: FilePreviewKinds.Loaded, lines: shown, truncated: totalLineCount > shown.length }; +} + +function loadPreviewCmd(preview: FileExplorerPreview, source: FilePreviewSource): Cmd { + return () => { + let result: FilePreviewResult; + try { result = source.loadFilePreview(preview.entry.path); } + catch { result = { kind: FilePreviewKinds.Unavailable }; } + return { type: WorkspaceMessageTypes.FileExplorerPreviewResult, ...preview, result }; + }; +} diff --git a/src/app/workspace/file-explorer-view.ts b/src/app/workspace/file-explorer-view.ts new file mode 100644 index 00000000..5ef41c14 --- /dev/null +++ b/src/app/workspace/file-explorer-view.ts @@ -0,0 +1,56 @@ +import { createSurface, stringToSurface, type Surface } from '@flyingrobots/bijou'; +import { fitBlock, formatTreeLine } from '../../ui/workspace-render.js'; +import { highlightFileSearchMatches } from '../../ui/file-search-results.js'; +import { renderFileSearchInput, type FileSearchInputOptions } from '../../ui/file-search-input.js'; +import type { WorkspaceModel } from './model.js'; +import { fileSearchEntries, fileSearchHasFocus, fileSearchState } from './file-search.js'; +import { + DRAWER_INNER_PAD, FILE_SEARCH_ROW, FILE_SEARCH_SEPARATOR_ROW, FILE_SEARCH_LIST_TOP, + fileSearchListHeight, listScrollOffset, +} from './viewport.js'; +import { applyBackground, fillSurface } from './surface-fill.js'; + +const PAD_MULTIPLIER = 2; +const MIN_WIDTH = 1; +const SEARCH_LABEL_KEY = 'fileExplorer.search_label'; +const SEARCH_PLACEHOLDER_KEY = 'fileExplorer.placeholder'; +const SEARCH_EMPTY_KEY = 'fileExplorer.empty'; +const INPUT_RULE = '─'; + +export function fileSearchInputOptions(model: WorkspaceModel, width: number): FileSearchInputOptions { + return { + state: fileSearchState(model), label: model.i18n.t(SEARCH_LABEL_KEY), + placeholder: model.i18n.t(SEARCH_PLACEHOLDER_KEY), focused: fileSearchHasFocus(model), + width, theme: model.jeditTheme, + }; +} + +export function renderFileExplorer(model: WorkspaceModel, width: number, height: number): Surface { + const surface = createSurface(width, height); + fillSurface(surface, model.jeditTheme.surface.drawer); + const innerWidth = Math.max(MIN_WIDTH, width - DRAWER_INNER_PAD * PAD_MULTIPLIER); + surface.blit(renderFileSearchInput(fileSearchInputOptions(model, innerWidth)), DRAWER_INNER_PAD, FILE_SEARCH_ROW); + const rule = stringToSurface(INPUT_RULE.repeat(innerWidth), innerWidth, 1); + applyBackground(rule, model.jeditTheme.surface.drawer); + surface.blit(rule, DRAWER_INNER_PAD, FILE_SEARCH_SEPARATOR_ROW); + paintFileSearchResults(surface, model, innerWidth, fileSearchListHeight(height)); + return surface; +} + +function paintFileSearchResults(surface: Surface, model: WorkspaceModel, width: number, height: number): void { + if (height <= 0) return; + const entries = fileSearchEntries(model); + const selected = entries.findIndex(({ index }) => index === model.selectedIndex); + const offset = listScrollOffset(selected, entries.length, height); + const lines = entries.length === 0 + ? [model.i18n.t(SEARCH_EMPTY_KEY)] + : entries.slice(offset, offset + height).map(({ entry, index }) => formatTreeLine(entry, { + selected: index === model.selectedIndex, + })); + const content = stringToSurface(fitBlock(lines.join('\n'), width, height), width, height); + applyBackground(content, model.jeditTheme.surface.drawer); + highlightFileSearchMatches(content, entries.slice(offset, offset + height), model.jeditTheme, { + selectedIndex: model.selectedIndex, hoveredIndex: model.fileHoveredIndex, + }); + surface.blit(content, DRAWER_INNER_PAD, FILE_SEARCH_LIST_TOP); +} diff --git a/src/app/workspace/file-search-input-key.ts b/src/app/workspace/file-search-input-key.ts new file mode 100644 index 00000000..29a09d33 --- /dev/null +++ b/src/app/workspace/file-search-input-key.ts @@ -0,0 +1,91 @@ +import type { Cmd, KeyMsg } from '@flyingrobots/bijou-tui'; +import { fileSearchHasFocus, fileExplorerHasFocus, fileSearchState, fileSearchWithInput, EMPTY_FILE_SEARCH, type FileSearchState } from './file-search.js'; +import type { WorkspaceModel } from './model.js'; +import type { WorkspaceMsg } from './msg.js'; + +const KEYS = Object.freeze({ + Left: 'left', Right: 'right', Home: 'home', End: 'end', Backspace: 'backspace', Delete: 'delete', + Space: 'space', Clear: 'u', Filter: '/', Escape: 'escape', Enter: 'enter', +}); +const LITERAL_SPACE = ' '; +const SINGLE_KEY_LENGTH = 1; +const NON_PRINTABLE = /[\p{Cc}\p{Cf}\p{Cs}\p{Zl}\p{Zp}]/u; +const FORWARD_STEP = 1; +const BACKWARD_STEP = -1; +type Edit = (state: FileSearchState) => FileSearchState; +const EDITS = new Map([ + [KEYS.Left, (state) => moveCursor(state, BACKWARD_STEP)], + [KEYS.Right, (state) => moveCursor(state, FORWARD_STEP)], + [KEYS.Home, (state) => ({ ...state, cursorIndex: 0 })], + [KEYS.End, (state) => ({ ...state, cursorIndex: Array.from(state.input).length })], + [KEYS.Backspace, deleteBeforeCursor], + [KEYS.Delete, deleteAtCursor], +]); + +export function updateFileSearchInputKey( + msg: KeyMsg, + model: WorkspaceModel, +): [WorkspaceModel, Cmd[]] | undefined { + if (!fileExplorerHasFocus(model)) return undefined; + const mode = updateFilterMode(msg, model); + if (mode != null) return mode; + if (!fileSearchHasFocus(model)) return undefined; + const state = fileSearchState(model); + const next = editFromKey(msg, state); + if (next == null) return undefined; + if (next.input !== state.input) return [fileSearchWithInput(model, next), []]; + return [next.cursorIndex === state.cursorIndex ? model : { ...model, fileSearch: next }, []]; +} + +function editFromKey(msg: KeyMsg, state: FileSearchState): FileSearchState | undefined { + if (isClearKey(msg)) return EMPTY_FILE_SEARCH; + if (msg.ctrl || msg.alt) return undefined; + const edit = EDITS.get(msg.key); + if (edit != null) return edit(state); + const input = printableInput(msg); + return input == null ? undefined : insertAtCursor(state, input); +} + +function isClearKey(msg: KeyMsg): boolean { + return msg.ctrl === true && !msg.alt && msg.key === KEYS.Clear; +} + +function printableInput(msg: KeyMsg): string | undefined { + const value = msg.key === KEYS.Space ? LITERAL_SPACE : msg.key; + return Array.from(value).length === SINGLE_KEY_LENGTH && !NON_PRINTABLE.test(value) + ? (msg.shift ? value.toUpperCase() : value) + : undefined; +} + +function moveCursor(state: FileSearchState, delta: number): FileSearchState { + return { ...state, cursorIndex: Math.max(0, Math.min(Array.from(state.input).length, state.cursorIndex + delta)) }; +} + +function insertAtCursor(state: FileSearchState, input: string): FileSearchState { + const characters = Array.from(state.input); + characters.splice(state.cursorIndex, 0, ...Array.from(input)); + return { input: characters.join(''), cursorIndex: state.cursorIndex + Array.from(input).length }; +} + +function deleteBeforeCursor(state: FileSearchState): FileSearchState { + if (state.cursorIndex <= 0) return state; + const characters = Array.from(state.input); + characters.splice(state.cursorIndex - FORWARD_STEP, FORWARD_STEP); + return { input: characters.join(''), cursorIndex: state.cursorIndex - FORWARD_STEP }; +} + +function deleteAtCursor(state: FileSearchState): FileSearchState { + const characters = Array.from(state.input); + characters.splice(state.cursorIndex, FORWARD_STEP); + return { input: characters.join(''), cursorIndex: state.cursorIndex }; +} + +function updateFilterMode(msg: KeyMsg, model: WorkspaceModel): [WorkspaceModel, Cmd[]] | undefined { + if (msg.ctrl || msg.alt) return undefined; + if (!model.fileSearchEditing) { + return msg.key === KEYS.Filter ? [{ ...model, fileSearchEditing: true }, []] : undefined; + } + return msg.key === KEYS.Escape || msg.key === KEYS.Enter + ? [{ ...model, fileSearchEditing: false }, []] + : undefined; +} diff --git a/src/app/workspace/file-search.ts b/src/app/workspace/file-search.ts new file mode 100644 index 00000000..148db04d --- /dev/null +++ b/src/app/workspace/file-search.ts @@ -0,0 +1,96 @@ +import type { FileEntry } from '../../ports/file-system.js'; +import { FileEntryKinds } from '../../ports/file-system.js'; +import { FocusPanes } from '../../ui/panel-focus.js'; +import { scrollIndexByRows } from '../../ui/mouse-scroll.js'; +import { wrapIndex } from '../list-index.js'; +import type { WorkspaceModel } from './model.js'; + +export interface FileSearchState { + readonly input: string; + readonly cursorIndex: number; +} + +export interface FileSearchEntry { + readonly entry: FileEntry; + readonly index: number; + readonly matchIndexes: readonly number[]; +} + +export const EMPTY_FILE_SEARCH: FileSearchState = Object.freeze({ input: '', cursorIndex: 0 }); +const PATH_SEPARATOR = '/'; +const NO_SELECTION = -1; + +export function fileSearchState(model: Pick): FileSearchState { + return model.fileSearch ?? EMPTY_FILE_SEARCH; +} + +export function fileExplorerHasFocus(model: WorkspaceModel): boolean { + return model.fileDrawerOpen && model.focusPane === FocusPanes.Files && + !model.settingsOpen && !model.quitConfirmOpen && !model.startupFileModalOpen && !model.commandLine?.active; +} + +export function fileSearchHasFocus(model: WorkspaceModel): boolean { + return fileExplorerHasFocus(model) && model.fileSearchEditing === true; +} + +export function fileSearchEntries(model: Pick): readonly FileSearchEntry[] { + const query = fileSearchState(model).input.toLowerCase(); + return model.entries.flatMap((entry, index) => { + if (query.length === 0) return [{ entry, index, matchIndexes: [] }]; + if (entry.kind === FileEntryKinds.Parent) return []; + const path = fuzzyPathMatches(entry.name, query) != null ? entry.name : relativeFilePath(model.workspaceSearchRoot ?? model.workspaceRoot, entry); + const matches = fuzzyPathMatches(path, query); + if (matches == null) return []; + const nameStart = Array.from(path).length - Array.from(entry.name).length; + return [{ entry, index, matchIndexes: matches.filter((offset) => offset >= nameStart).map((offset) => offset - nameStart) }]; + }); +} + +function relativeFilePath(root: string, entry: FileEntry): string { + const prefix = root?.endsWith(PATH_SEPARATOR) ? root : `${root ?? ''}${PATH_SEPARATOR}`; + const path = entry.searchPath ?? entry.path ?? entry.name; + // Outside the search root (after following `..` upward) only the name is + // searchable; the absolute path would leak ancestor text into every match. + return path.startsWith(prefix) ? path.slice(prefix.length) : entry.name; +} + +function fuzzyPathMatches(path: string, query: string): readonly number[] | undefined { + const candidate = Array.from(path).flatMap((character, index) => ( + Array.from(character.toLowerCase()).map((value) => ({ value, index })) + )); + const matches: number[] = []; + let offset = 0; + for (const character of query) { + while (offset < candidate.length && candidate[offset]?.value !== character) offset += 1; + const matched = candidate[offset]; + if (matched == null) return undefined; + matches.push(matched.index); + offset += 1; + } + return matches; +} + +export function selectedFileSearchEntry(model: WorkspaceModel): FileEntry | undefined { + return fileSearchEntries(model).find(({ index }) => index === model.selectedIndex)?.entry; +} + +export function fileSearchWithInput(model: WorkspaceModel, search: FileSearchState): WorkspaceModel { + const next = { ...model, fileSearch: search, fileHoveredIndex: undefined }; + return { ...next, selectedIndex: fileSearchEntries(next)[0]?.index ?? NO_SELECTION }; +} + +export function moveFileSearchSelection(model: WorkspaceModel, delta: number): WorkspaceModel { + const entries = fileSearchEntries(model); + if (entries.length === 0) return model; + const position = entries.findIndex(({ index }) => index === model.selectedIndex); + const next = entries[wrapIndex(position + delta, entries.length)]; + return next == null ? model : { ...model, selectedIndex: next.index, fileHoveredIndex: undefined }; +} + +export function scrollFileSearchSelection(model: WorkspaceModel, delta: number): WorkspaceModel { + const entries = fileSearchEntries(model); + if (entries.length === 0) return model; + const position = entries.findIndex(({ index }) => index === model.selectedIndex); + const next = entries[scrollIndexByRows(position, entries.length, delta)]; + return next == null ? model : { ...model, selectedIndex: next.index, fileHoveredIndex: undefined }; +} diff --git a/src/app/workspace/file-tree.ts b/src/app/workspace/file-tree.ts index bb193929..139df40d 100644 --- a/src/app/workspace/file-tree.ts +++ b/src/app/workspace/file-tree.ts @@ -10,7 +10,6 @@ import type { EditorFilePort } from '../../ports/editor-file.js'; import type { GraftSessionPort } from '../../ports/graft-session.js'; import type { SourceHighlighter } from '../../ports/source-highlighter.js'; import { createNotificationTickCmd, pushErrorToast } from '../../ui/feedback.js'; -import { wrapIndex } from './viewport.js'; import { withFocusPane } from './focus.js'; import { isWorkspaceMarkdownFile } from './editor-session.js'; import { ViewModes } from './view-mode.js'; @@ -20,7 +19,9 @@ import { WorkspaceMessageTypes, type WorkspaceMsg } from './msg.js'; import type { ProductionTextSession } from './production-text-session.js'; import { createWorkspaceTextOpenCmd, defaultWorkspaceTextAperture } from './workspace-text-commands.js'; import { pendingWorkspaceTextOpen } from './workspace-text-authority.js'; +import { EMPTY_FILE_SEARCH, fileSearchState, fileSearchWithInput, moveFileSearchSelection, selectedFileSearchEntry } from './file-search.js'; import { + WorkspaceKeys, isWorkspaceBackKey, isWorkspaceDownKey, isWorkspaceOpenKey, @@ -35,6 +36,8 @@ import { } from './workspace-buffer-registry.js'; export const FILE_TREE_META_MIN = 0; +const FILE_TREE_FORWARD_STEP = 1; +const FILE_TREE_BACKWARD_STEP = -1; export interface UpdateTreeFromKeyDeps { readonly fileSystem: FileSystemPort; @@ -50,6 +53,8 @@ export function updateTreeFromKey( nowMs: () => number, deps: UpdateTreeFromKeyDeps, ): [WorkspaceModel, Cmd[]] { + const modified = updateModifiedTreeKey(msg, model, nowMs, deps.fileSystem); + if (modified != null) return modified; const navigation = updateTreeNavigationFromKey(msg, model); if (navigation != null) { return [navigation, []]; @@ -66,18 +71,29 @@ export function updateTreeFromKey( : [model, []]; } +function updateModifiedTreeKey( + msg: KeyMsg, + model: WorkspaceModel, + nowMs: () => number, + fileSystem: FileSystemPort, +): [WorkspaceModel, Cmd[]] | undefined { + if (msg.alt && msg.key === WorkspaceKeys.ArrowUp) { + return openParentDirectory(model, nowMs, fileSystem); + } + if (msg.ctrl || msg.alt) { + return msg.ctrl && msg.key === WorkspaceKeys.R + ? changeDirectory(model, model.cwd, DIRECTORY_ACTION_REFRESH, nowMs, fileSystem) + : [model, []]; + } + return undefined; +} + function updateTreeNavigationFromKey(msg: KeyMsg, model: WorkspaceModel): WorkspaceModel | undefined { if (isWorkspaceDownKey(msg)) { - return { - ...model, - selectedIndex: wrapIndex(model.selectedIndex + 1, model.entries.length), - }; + return moveFileSearchSelection(model, FILE_TREE_FORWARD_STEP); } if (isWorkspaceUpKey(msg)) { - return { - ...model, - selectedIndex: wrapIndex(model.selectedIndex - 1, model.entries.length), - }; + return moveFileSearchSelection(model, FILE_TREE_BACKWARD_STEP); } return undefined; } @@ -98,7 +114,7 @@ function openSelectedTreeEntry( nowMs: () => number, deps: UpdateTreeFromKeyDeps, ): [WorkspaceModel, Cmd[]] { - const entry = model.entries[model.selectedIndex]; + const entry = selectedFileSearchEntry(model); if (entry == null) { return [model, []]; } @@ -152,12 +168,12 @@ function openDirectory( cwd: string, fileSystem: FileSystemPort, ): WorkspaceModel { - return { + return fileSearchWithInput({ ...model, cwd, entries: fileSystem.loadEntries(cwd), selectedIndex: 0, - }; + }, cwd === model.cwd ? fileSearchState(model) : EMPTY_FILE_SEARCH); } function changeDirectory( diff --git a/src/app/workspace/global-key-bindings.ts b/src/app/workspace/global-key-bindings.ts index 2de245f0..98bffbf8 100644 --- a/src/app/workspace/global-key-bindings.ts +++ b/src/app/workspace/global-key-bindings.ts @@ -91,7 +91,7 @@ function updateFocusKey(msg: KeyMsg, model: WorkspaceModel): KeyBindingResult | if (msg.key !== WorkspaceKeys.Tab || !hasFocusablePeers(focusState)) { return undefined; } - return [{ ...model, focusPane: cycleFocusPane(focusState) }, []]; + return [{ ...model, focusPane: cycleFocusPane(focusState), fileSearchEditing: false }, []]; } function updateDrawerKey( diff --git a/src/app/workspace/init.ts b/src/app/workspace/init.ts index 51a13a6d..a7fd4c33 100644 --- a/src/app/workspace/init.ts +++ b/src/app/workspace/init.ts @@ -9,6 +9,7 @@ import { ViewModes } from "./view-mode.js"; import { TEXT_RUNTIME_PROFILE_ECHO_HOSTED } from "../text-runtime-profile.js"; import { createWorkspaceTextAuthority } from "./workspace-text-authority.js"; import { emptyWorkspaceBufferRegistry } from "./workspace-buffer-registry.js"; +import { EMPTY_FILE_SEARCH } from './file-search.js'; import { initialStartupFileModalState, INITIAL_WORKSPACE_CAUSAL_GUTTER_BASIS, @@ -26,6 +27,7 @@ export interface WorkspaceInitialModelSnapshot { readonly i18n: I18nPort; readonly entries: readonly FileEntry[]; readonly nowMs: number; + readonly searchRoot?: string; } export function createInitialModel( @@ -39,9 +41,13 @@ export function createInitialModel( return { i18n, workspaceRoot: cwd, + workspaceSearchRoot: snapshot.searchRoot, cwd, entries, selectedIndex: 0, + fileSearch: EMPTY_FILE_SEARCH, + fileSearchEditing: false, + fileExplorerPreviewRequestId: 0, editor: undefined, ...initialBufferRegistryState(), textRuntimeProfile, diff --git a/src/app/workspace/key-bindings.ts b/src/app/workspace/key-bindings.ts index 7644d04b..a5440d67 100644 --- a/src/app/workspace/key-bindings.ts +++ b/src/app/workspace/key-bindings.ts @@ -13,6 +13,7 @@ import { clearWorkspaceInlinePanelAfterKey } from './workspace-inline-panel.js'; import { updateSettingsKey } from './settings-key-bindings.js'; import { updateStartupFileModalKey } from './startup-file-modal-key-bindings.js'; import { updateTitleScreenKey } from './title-screen-key-bindings.js'; +import { updateFileSearchInputKey } from './file-search-input-key.js'; export type { UpdateFromKeyDeps } from './key-binding-context.js'; @@ -24,6 +25,7 @@ export function updateFromKey( context: WorkspaceKeyBindingContext, ): KeyBindingResult { const result = updateQuitConfirmationKey(msg, model) + ?? updateFileSearchInputKey(msg, model) ?? updateHardGlobalWorkspaceKey(msg, model) ?? updateWorkspaceOverlayKey(msg, model, context) ?? updateGlobalWorkspaceKey(msg, model, context) diff --git a/src/app/workspace/model.ts b/src/app/workspace/model.ts index 618bf281..43deca39 100644 --- a/src/app/workspace/model.ts +++ b/src/app/workspace/model.ts @@ -21,15 +21,23 @@ import type { } from "./command-completion-preview.js"; import type { WorkspaceInlinePanel } from "./workspace-inline-panel.js"; import type { WorkspaceCausalGutterBasis } from "./workspace-causal-gutter-basis.js"; +import type { FileExplorerPreview } from './file-explorer-preview.js'; +import type { FileSearchState } from './file-search.js'; export interface WorkspaceModel extends StartupFileModalState, WorkspaceCommandLineFilePreviewState { readonly i18n: I18nPort; readonly workspaceRoot: string; + readonly workspaceSearchRoot?: string; readonly cwd: string; readonly entries: readonly FileEntry[]; readonly selectedIndex: number; + readonly fileSearch: FileSearchState; + readonly fileSearchEditing: boolean; + readonly fileHoveredIndex?: number; + readonly fileExplorerPreview?: FileExplorerPreview; + readonly fileExplorerPreviewRequestId: number; readonly editor?: EditorState; readonly buffers: WorkspaceBufferRegistry; readonly activeBufferId?: string; diff --git a/src/app/workspace/mouse.ts b/src/app/workspace/mouse.ts index 3c0e507d..06276881 100644 --- a/src/app/workspace/mouse.ts +++ b/src/app/workspace/mouse.ts @@ -17,35 +17,33 @@ import { listScrollOffset, workspaceBodyHeight, WORKSPACE_BODY_TOP_OFFSET, + FILE_SEARCH_ROW, + FILE_SEARCH_LIST_TOP, + fileSearchListHeight, } from './viewport.js'; import { FocusPanes } from '../../ui/panel-focus.js'; import { beginWorkspaceSourceHighlightRefresh } from './workspace-source-highlight.js'; +import { fileSearchEntries, fileSearchState, scrollFileSearchSelection } from './file-search.js'; +import { fileSearchCursorAtColumn } from '../../ui/file-search-input.js'; +import { fileSearchInputOptions } from './file-explorer-view.js'; const DRAWER_PAD_MULTIPLIER = 2; const MOUSE_PRESS = 'press'; +const MOUSE_MOVE = 'move'; const MOUSE_BUTTON_LEFT = 'left'; -// Opening on click needs the filesystem and Echo dependencies the pointer path -// does not otherwise carry. Injecting a named opener keeps that dependency -// explicit and lets the selection-only behaviour stand on its own when the -// caller has nothing to open with. -export interface WorkspaceEntryOpener { - readonly nowMs: () => number; - readonly openEntry: ( - model: WorkspaceModel, - entry: NonNullable, - ) => [WorkspaceModel, Cmd[]]; -} - export function updateFromMouse( msg: MouseMsg, model: WorkspaceModel, sourceHighlighter: SourceHighlighter, - opener?: WorkspaceEntryOpener, ): [WorkspaceModel, Cmd[]] { + const hover = updateFileDrawerHover(msg, model); + if (hover != null) return [hover, []]; + const input = updateFileSearchFromClick(msg, model); + if (input != null) return [input, []]; const clicked = updateFileDrawerFromClick(msg, model); if (clicked != null) { - return openClickedEntry(clicked, opener); + return [clicked, []]; } const deltaRows = mouseScrollDeltaRows(msg); if (deltaRows === 0) { @@ -58,10 +56,10 @@ export function updateFromMouse( return updateEditorFromMouse(model, deltaRows, sourceHighlighter); } -// Row and column map onto the list the renderer drew: the drawer is blitted at +// Row and column map onto the pinned list the renderer drew: the drawer is blitted at // (layout.fileDrawer.x, WORKSPACE_BODY_TOP_OFFSET) and its content is inset by -// DRAWER_INNER_PAD. Entry index equals line index because the list does not -// scroll -- long directories are truncated rather than paged today. +// DRAWER_INNER_PAD. Filtered positions map through the scroll offset back to +// original entry indexes. function updateFileDrawerFromClick( msg: MouseMsg, model: WorkspaceModel, @@ -72,14 +70,14 @@ function updateFileDrawerFromClick( const index = fileDrawerEntryIndexAt(msg, model); return index == null ? undefined - : { ...model, focusPane: FocusPanes.Files, selectedIndex: index }; + : { ...model, focusPane: FocusPanes.Files, selectedIndex: index, fileSearchEditing: false }; } function fileDrawerEntryIndexAt( msg: MouseMsg, model: WorkspaceModel, ): number | undefined { - if (!model.fileDrawerOpen) { + if (!fileDrawerPointerEnabled(model)) { return undefined; } const drawer = resolveWorkspaceLayout( @@ -93,31 +91,28 @@ function fileDrawerEntryIndexAt( if (!withinDrawer) { return undefined; } - const row = msg.row - WORKSPACE_BODY_TOP_OFFSET - DRAWER_INNER_PAD; - const listHeight = workspaceBodyHeight({ + const row = msg.row - WORKSPACE_BODY_TOP_OFFSET - FILE_SEARCH_LIST_TOP; + const listHeight = fileSearchListHeight(workspaceBodyHeight({ rows: model.rows, footerVisible: model.footerVisible, - }) - (DRAWER_INNER_PAD * DRAWER_PAD_MULTIPLIER); + })); if (row < 0 || row >= listHeight) { return undefined; } + const entries = fileSearchEntries(model); + const position = entries.findIndex(({ index }) => index === model.selectedIndex); const index = row + listScrollOffset( - model.selectedIndex, - model.entries.length, + position, + entries.length, listHeight, ); - return index < model.entries.length ? index : undefined; + return entries[index]?.index; } -function openClickedEntry( - model: WorkspaceModel, - opener: WorkspaceEntryOpener | undefined, -): [WorkspaceModel, Cmd[]] { - const entry = model.entries[model.selectedIndex]; - if (opener == null || entry == null) { - return [model, []]; - } - return opener.openEntry(model, entry); +function updateFileDrawerHover(msg: MouseMsg, model: WorkspaceModel): WorkspaceModel | undefined { + if (msg.action !== MOUSE_MOVE) return undefined; + const index = fileDrawerEntryIndexAt(msg, model); + return model.fileHoveredIndex === index ? model : { ...model, fileHoveredIndex: index }; } function updateScrollableDrawerFromMouse(model: WorkspaceModel, deltaRows: number): WorkspaceModel | undefined { @@ -128,7 +123,7 @@ function updateScrollableDrawerFromMouse(model: WorkspaceModel, deltaRows: numbe }; } if (model.focusPane === FocusPanes.Files && model.fileDrawerOpen) { - return { ...model, selectedIndex: scrollIndexByRows(model.selectedIndex, model.entries.length, deltaRows) }; + return scrollFileSearchSelection(model, deltaRows); } if (model.focusPane === FocusPanes.Graft && model.graftDrawerOpen) { return { @@ -139,6 +134,27 @@ function updateScrollableDrawerFromMouse(model: WorkspaceModel, deltaRows: numbe return undefined; } +function fileDrawerPointerEnabled(model: WorkspaceModel): boolean { + return model.fileDrawerOpen && !model.settingsOpen && !model.quitConfirmOpen && + !model.startupFileModalOpen && !model.commandLine?.active; +} + +function updateFileSearchFromClick(msg: MouseMsg, model: WorkspaceModel): WorkspaceModel | undefined { + if (msg.action !== MOUSE_PRESS || msg.button !== MOUSE_BUTTON_LEFT || !fileDrawerPointerEnabled(model)) return undefined; + if (msg.row !== WORKSPACE_BODY_TOP_OFFSET + FILE_SEARCH_ROW) return undefined; + const layout = resolveWorkspaceLayout(model.columns, model.fileDrawerProgress, model.graftDrawerProgress).fileDrawer; + const innerWidth = layout.width - DRAWER_INNER_PAD * DRAWER_PAD_MULTIPLIER; + const column = msg.col - layout.x - DRAWER_INNER_PAD; + if (column < 0 || column >= innerWidth) return undefined; + return { + ...model, focusPane: FocusPanes.Files, fileSearchEditing: true, + fileSearch: { + ...fileSearchState(model), + cursorIndex: fileSearchCursorAtColumn(fileSearchInputOptions(model, innerWidth), column), + }, + }; +} + function updateEditorFromMouse( model: WorkspaceModel, deltaRows: number, diff --git a/src/app/workspace/msg.ts b/src/app/workspace/msg.ts index 66f5b733..7e615f76 100644 --- a/src/app/workspace/msg.ts +++ b/src/app/workspace/msg.ts @@ -1,3 +1,4 @@ +import type { FileExplorerPreviewMessage } from './file-explorer-preview.js'; import type { RuntimeIssue } from "@flyingrobots/bijou-tui"; import type { GraftDiagnosticsReport } from "../../ports/graft-diagnostics.js"; import type { GraftInfo } from "../../ports/graft-session.js"; @@ -50,6 +51,7 @@ const WORKSPACE_INPUT_MESSAGE_KEY = "key"; const WORKSPACE_INPUT_MESSAGE_MOUSE = "mouse"; export const WorkspaceMessageTypes = Object.freeze({ + FileExplorerPreviewResult: 'file-explorer-preview-result', DrawerProgress: WORKSPACE_MESSAGE_DRAWER_PROGRESS, GraftDiagnostics: WORKSPACE_MESSAGE_GRAFT_DIAGNOSTICS, GraftInfo: WORKSPACE_MESSAGE_GRAFT_INFO, @@ -96,6 +98,7 @@ export type WorkspaceWhyRangeOutcome = }; export type WorkspaceMsg = + | ({ readonly type: typeof WorkspaceMessageTypes.FileExplorerPreviewResult } & FileExplorerPreviewMessage) | { type: typeof WorkspaceMessageTypes.DrawerProgress; kind: DrawerKind; diff --git a/src/app/workspace/runtime.ts b/src/app/workspace/runtime.ts index de5defe0..563a9a47 100644 --- a/src/app/workspace/runtime.ts +++ b/src/app/workspace/runtime.ts @@ -1,3 +1,4 @@ +import { applyFileExplorerPreview, synchronizeFileExplorerPreview } from './file-explorer-preview.js'; import { createInitialModel, workspaceAnimationIsActive } from "./init.js"; import type { WorkspaceModel } from "./model.js"; import { @@ -84,9 +85,9 @@ function updateWorkspaceRuntime( msg: WorkspaceRuntimeMsg, model: WorkspaceModel, ): WorkspaceRuntimeResult { - return syncWorkspaceRuntimeResult( + return synchronizeFileExplorerPreview(syncWorkspaceRuntimeResult( rebaseFrameClock(deps, model, updateWorkspaceRuntimeState(deps, msg, model)), - ); + ), deps.filePreviewSource); } // While the workspace is idle the tick handler returns the model untouched, so @@ -199,6 +200,7 @@ function updateWorkspaceStateMessage( msg: WorkspaceRuntimeMsg, model: WorkspaceModel, ): WorkspaceRuntimeResult | undefined { + if (msg.type === WorkspaceMessageTypes.FileExplorerPreviewResult) return [applyFileExplorerPreview(model, msg), []]; const animation = updateAnimationStateMessage(msg, model); if (animation != null) { return animation; @@ -400,9 +402,6 @@ function updateWorkspaceInputMessage( msg, model, deps.sourceHighlighter, - deps.openEntry == null - ? undefined - : { nowMs: deps.nowMs, openEntry: deps.openEntry }, ); } return msg.type === WorkspaceInputMessageTypes.Key diff --git a/src/app/workspace/settings-key-bindings.ts b/src/app/workspace/settings-key-bindings.ts index b6cdb59c..c6144fd1 100644 --- a/src/app/workspace/settings-key-bindings.ts +++ b/src/app/workspace/settings-key-bindings.ts @@ -35,6 +35,7 @@ export function updateSettingsKey( return [{ ...model, settingsOpen: !model.settingsOpen, + fileSearchEditing: false, settingsFocusIndex: 0, settingsDiagnosticsOpen: false, }, []]; diff --git a/src/app/workspace/viewer-content.ts b/src/app/workspace/viewer-content.ts index a02d4450..3369e689 100644 --- a/src/app/workspace/viewer-content.ts +++ b/src/app/workspace/viewer-content.ts @@ -1,3 +1,4 @@ +import { renderFileExplorerPreview } from './file-explorer-preview-view.js'; import { createSurface, type Surface } from '@flyingrobots/bijou'; import { paintMarkdownPreview } from '../../ui/markdown-preview.js'; import { renderSourceViewer } from '../../ui/source-viewer.js'; @@ -27,6 +28,8 @@ export function createViewerContentRenderer(): ViewerContentRenderer { } export function renderViewer(model: WorkspaceModel, width: number, height: number): Surface { + const explorerPreview = renderFileExplorerPreview(model, width, height); + if (explorerPreview != null) return explorerPreview; const editor = displayEditorForWorkspaceModel(model); if (editor == null) { return renderJimLogoScreen(width, height, model.jeditTheme); diff --git a/src/app/workspace/viewer-drawers.ts b/src/app/workspace/viewer-drawers.ts index 5483dc56..57503612 100644 --- a/src/app/workspace/viewer-drawers.ts +++ b/src/app/workspace/viewer-drawers.ts @@ -3,11 +3,11 @@ import { DrawerKinds, type DrawerKind } from '../../ui/drawer-layout.js'; import { renderGraftDrawerLines } from '../../ui/graft-drawer.js'; import { fitBlock, - formatTreeLine, } from '../../ui/workspace-render.js'; import type { WorkspaceModel } from './model.js'; -import { DRAWER_INNER_PAD, listScrollOffset } from './viewport.js'; +import { DRAWER_INNER_PAD } from './viewport.js'; import { applyBackground, fillSurface } from './surface-fill.js'; +import { renderFileExplorer } from './file-explorer-view.js'; const MIN_VIEWPORT_DIMENSION = 1; const DRAWER_PAD_MULTIPLIER = 2; @@ -16,26 +16,7 @@ export function renderDrawer(kind: DrawerKind, model: WorkspaceModel, width: num if (kind === DrawerKinds.Graft) { return renderGraftDrawer(model, width, height); } - const surface = createSurface(width, height); - fillSurface(surface, model.jeditTheme.surface.drawer); - - const listWidth = Math.max(MIN_VIEWPORT_DIMENSION, width - (DRAWER_INNER_PAD * DRAWER_PAD_MULTIPLIER)); - const listHeight = Math.max(MIN_VIEWPORT_DIMENSION, height - (DRAWER_INNER_PAD * DRAWER_PAD_MULTIPLIER)); - const offset = listScrollOffset( - model.selectedIndex, - model.entries.length, - listHeight, - ); - const lines = model.entries - .slice(offset, offset + listHeight) - .map((entry, index) => formatTreeLine(entry, { - selected: index + offset === model.selectedIndex, - })); - const content = stringToSurface(fitBlock(lines.join('\n'), listWidth, listHeight), listWidth, listHeight); - applyBackground(content, model.jeditTheme.surface.drawer); - surface.blit(content, DRAWER_INNER_PAD, DRAWER_INNER_PAD); - - return surface; + return renderFileExplorer(model, width, height); } function renderGraftDrawer(model: WorkspaceModel, width: number, height: number): Surface { diff --git a/src/app/workspace/viewer.ts b/src/app/workspace/viewer.ts index 81b8cc9e..f372bf9b 100644 --- a/src/app/workspace/viewer.ts +++ b/src/app/workspace/viewer.ts @@ -132,6 +132,7 @@ function paintWorkspaceFooter(screen: Surface, model: WorkspaceModel): void { i18n: model.i18n, focusPane: model.focusPane, fileDrawerOpen: model.fileDrawerOpen, + fileSearchEditing: model.fileSearchEditing, graftDrawerOpen: model.graftDrawerOpen, viewMode: model.viewMode, markdownPreviewActive: isWorkspaceMarkdownPreviewAvailable(model), diff --git a/src/app/workspace/viewport.ts b/src/app/workspace/viewport.ts index b69fc0ea..892b61a0 100644 --- a/src/app/workspace/viewport.ts +++ b/src/app/workspace/viewport.ts @@ -8,6 +8,13 @@ export const MIN_ROWS = 12; export const VIEWER_LEFT_PAD = 4; export const VIEWER_TOP_PAD = 1; export const DRAWER_INNER_PAD = 1; +export const FILE_SEARCH_ROW = DRAWER_INNER_PAD; +export const FILE_SEARCH_SEPARATOR_ROW = FILE_SEARCH_ROW + 1; +export const FILE_SEARCH_LIST_TOP = FILE_SEARCH_SEPARATOR_ROW + 1; + +export function fileSearchListHeight(height: number): number { + return Math.max(0, height - FILE_SEARCH_LIST_TOP - DRAWER_INNER_PAD); +} // Rows above the workspace body: the title row and its rule. Shared so pointer // hit-testing and the renderer cannot disagree about where the body starts. export const WORKSPACE_BODY_TOP_OFFSET = 2; diff --git a/src/app/workspace/workspace-runtime-dependencies.ts b/src/app/workspace/workspace-runtime-dependencies.ts index 13a62c31..be362fb4 100644 --- a/src/app/workspace/workspace-runtime-dependencies.ts +++ b/src/app/workspace/workspace-runtime-dependencies.ts @@ -1,3 +1,4 @@ +import type { FilePreviewSource } from '../../ports/file-preview.js'; import type { Cmd, KeyMsg, MouseMsg, ResizeMsg, RuntimeIssue } from '@flyingrobots/bijou-tui'; import type { DrawerKind } from '../../ui/drawer-layout.js'; import type { EditorFilePort } from '../../ports/editor-file.js'; @@ -12,17 +13,13 @@ import type { WorkspaceMsg } from './msg.js'; import type { ProductionTextSession } from './production-text-session.js'; import type { ProfilerTracePort } from '../raytracer-profiler.js'; import type { renderWorkspace } from './viewer.js'; -import type { WorkspaceEntryOpener } from './mouse.js'; export type WorkspaceRuntimeMsg = WorkspaceMsg | ResizeMsg | KeyMsg | MouseMsg; export type WorkspaceRuntimeResult = [WorkspaceModel, Cmd[]]; export type WorkspaceResizeMsg = ResizeMsg; export interface WorkspaceRuntimeDependencies { - // Supplied by the composition root so pointer clicks can open an entry. - // Optional: the pointer path still selects without it, and test harnesses - // that never click do not have to construct the filesystem dependencies. - readonly openEntry?: WorkspaceEntryOpener['openEntry']; + readonly filePreviewSource?: FilePreviewSource; readonly initialColumns: number; readonly initialRows: number; readonly initialWorkingDirectory: string; diff --git a/src/generated/i18n.ts b/src/generated/i18n.ts index 5cbbfc1b..b5b8fbec 100644 --- a/src/generated/i18n.ts +++ b/src/generated/i18n.ts @@ -78,6 +78,9 @@ export type TranslationSchema = { readonly "y_end": string; readonly "gg_top": string; readonly "esc_cancel": string; + readonly "type_filter": string; + readonly "arrows_select": string; + readonly "slash_filter": string; }; readonly "evidence": { readonly "head": string; @@ -167,6 +170,16 @@ export type TranslationSchema = { readonly "why": { readonly "range_obstructed_title": string; }; + readonly "fileExplorer": { + readonly "search_label": string; + readonly "placeholder": string; + readonly "empty": string; + readonly "preview_label": string; + readonly "preview_loading": string; + readonly "preview_binary": string; + readonly "preview_unavailable": string; + readonly "preview_truncated": string; + }; }; export type LocaleDirection = 'ltr' | 'rtl'; @@ -176,47 +189,47 @@ export interface LocaleMetadata { readonly direction: LocaleDirection; } -export const en: TranslationSchema = {"footer":{"mode":{"browse":"browse","insert":"insert","normal":"normal","preview":"preview","settings":"settings","files":"files","graft":"graft","history":"history"},"context":{"settings":"settings","graft_empty":"open a file to inspect it","history_empty":"no Echo evidence yet","history_count":"Echo evidence: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k move","enter_change":"enter change","f2_close":"f2 close","esc_close":"esc close","j_k_scroll":"j/k scroll","f3_source":"f3 source","f3_preview":"f3 preview","text_input":"text input","esc_normal":"esc normal","ctrl_s_save":"ctrl+s save","ctrl_t_theme":"ctrl+t theme","tab_focus":"tab focus","tab_indent":"tab indent","ctrl_b_files":"ctrl+b files","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h history","enter_open":"enter open","backspace_up":"backspace up","ctrl_b_close":"ctrl+b close","enter_jump":"enter jump","r_refresh":"r refresh","ctrl_g_close":"ctrl+g close","ctrl_h_close":"ctrl+h close","i_insert":"i insert","o_open_line":"o open line","cc_line":"cc line","cw_word":"cw word","ce_word_end":"ce word-end","c0_start":"c0 start","c_end":"c$ end","dd_line":"dd line","dw_word":"dw word","de_word_end":"de word-end","d0_start":"d0 start","d_end":"d$ end","yy_line":"yy line","yw_word":"yw word","ye_word_end":"ye word-end","y0_start":"y0 start","y_end":"y$ end","gg_top":"gg top","esc_cancel":"esc cancel"},"evidence":{"head":"head","tick":"tick","anchor":"anchor","coverage":"coverage","complete":"complete","partial":"partial","unavailable":"unavailable"}},"history":{"title":"Echo History","empty":"No Echo evidence yet","header":"# tick kind status evidence summary"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const en: TranslationSchema = {"footer":{"mode":{"browse":"browse","insert":"insert","normal":"normal","preview":"preview","settings":"settings","files":"files","graft":"graft","history":"history"},"context":{"settings":"settings","graft_empty":"open a file to inspect it","history_empty":"no Echo evidence yet","history_count":"Echo evidence: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k move","enter_change":"enter change","f2_close":"f2 close","esc_close":"esc close","j_k_scroll":"j/k scroll","f3_source":"f3 source","f3_preview":"f3 preview","text_input":"text input","esc_normal":"esc normal","ctrl_s_save":"ctrl+s save","ctrl_t_theme":"ctrl+t theme","tab_focus":"tab focus","tab_indent":"tab indent","ctrl_b_files":"ctrl+b files","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h history","enter_open":"enter open","backspace_up":"backspace up","ctrl_b_close":"ctrl+b close","enter_jump":"enter jump","r_refresh":"r refresh","ctrl_g_close":"ctrl+g close","ctrl_h_close":"ctrl+h close","i_insert":"i insert","o_open_line":"o open line","cc_line":"cc line","cw_word":"cw word","ce_word_end":"ce word-end","c0_start":"c0 start","c_end":"c$ end","dd_line":"dd line","dw_word":"dw word","de_word_end":"de word-end","d0_start":"d0 start","d_end":"d$ end","yy_line":"yy line","yw_word":"yw word","ye_word_end":"ye word-end","y0_start":"y0 start","y_end":"y$ end","gg_top":"gg top","esc_cancel":"esc cancel","type_filter":"type filter","arrows_select":"↑/↓ select","slash_filter":"/ filter"},"evidence":{"head":"head","tick":"tick","anchor":"anchor","coverage":"coverage","complete":"complete","partial":"partial","unavailable":"unavailable"}},"history":{"title":"Echo History","empty":"No Echo evidence yet","header":"# tick kind status evidence summary"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Search","placeholder":"Filter files…","empty":"No matches","preview_label":"Preview","preview_loading":"Loading preview…","preview_binary":"Binary file — preview unavailable","preview_unavailable":"Preview unavailable","preview_truncated":"Preview truncated"}}; -export const fr: TranslationSchema = {"footer":{"mode":{"browse":"parcourir","insert":"insertion","normal":"normal","preview":"aperçu","settings":"paramètres","files":"fichiers","graft":"graft","history":"historique"},"context":{"settings":"paramètres","graft_empty":"ouvrez un fichier pour l'inspecter","history_empty":"pas encore de preuve Echo","history_count":"Preuves Echo : {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k déplacer","enter_change":"enter changer","f2_close":"f2 fermer","esc_close":"esc fermer","j_k_scroll":"j/k défiler","f3_source":"f3 source","f3_preview":"f3 aperçu","text_input":"saisie texte","esc_normal":"esc normal","ctrl_s_save":"ctrl+s enregistrer","ctrl_t_theme":"ctrl+t thème","tab_focus":"tab focus","tab_indent":"tab retrait","ctrl_b_files":"ctrl+b fichiers","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historique","enter_open":"enter ouvrir","backspace_up":"backspace haut","ctrl_b_close":"ctrl+b fermer","enter_jump":"enter aller","r_refresh":"r actualiser","ctrl_g_close":"ctrl+g fermer","ctrl_h_close":"ctrl+h fermer","i_insert":"i insérer","o_open_line":"o ouvrir ligne","cc_line":"cc ligne","cw_word":"cw mot","ce_word_end":"ce fin mot","c0_start":"c0 début","c_end":"c$ fin","dd_line":"dd ligne","dw_word":"dw mot","de_word_end":"de fin mot","d0_start":"d0 début","d_end":"d$ fin","yy_line":"yy ligne","yw_word":"yw mot","ye_word_end":"ye fin mot","y0_start":"y0 début","y_end":"y$ fin","gg_top":"gg haut","esc_cancel":"esc annuler"},"evidence":{"head":"tête","tick":"tick","anchor":"ancre","coverage":"couverture","complete":"complète","partial":"partielle","unavailable":"indisponible"}},"history":{"title":"Historique Echo","empty":"Pas encore de preuve Echo","header":"# tick type statut preuve résumé"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const fr: TranslationSchema = {"footer":{"mode":{"browse":"parcourir","insert":"insertion","normal":"normal","preview":"aperçu","settings":"paramètres","files":"fichiers","graft":"graft","history":"historique"},"context":{"settings":"paramètres","graft_empty":"ouvrez un fichier pour l'inspecter","history_empty":"pas encore de preuve Echo","history_count":"Preuves Echo : {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k déplacer","enter_change":"enter changer","f2_close":"f2 fermer","esc_close":"esc fermer","j_k_scroll":"j/k défiler","f3_source":"f3 source","f3_preview":"f3 aperçu","text_input":"saisie texte","esc_normal":"esc normal","ctrl_s_save":"ctrl+s enregistrer","ctrl_t_theme":"ctrl+t thème","tab_focus":"tab focus","tab_indent":"tab retrait","ctrl_b_files":"ctrl+b fichiers","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historique","enter_open":"enter ouvrir","backspace_up":"backspace haut","ctrl_b_close":"ctrl+b fermer","enter_jump":"enter aller","r_refresh":"r actualiser","ctrl_g_close":"ctrl+g fermer","ctrl_h_close":"ctrl+h fermer","i_insert":"i insérer","o_open_line":"o ouvrir ligne","cc_line":"cc ligne","cw_word":"cw mot","ce_word_end":"ce fin mot","c0_start":"c0 début","c_end":"c$ fin","dd_line":"dd ligne","dw_word":"dw mot","de_word_end":"de fin mot","d0_start":"d0 début","d_end":"d$ fin","yy_line":"yy ligne","yw_word":"yw mot","ye_word_end":"ye fin mot","y0_start":"y0 début","y_end":"y$ fin","gg_top":"gg haut","esc_cancel":"esc annuler","type_filter":"saisir le filtre","arrows_select":"↑/↓ sélectionner","slash_filter":"/ filtrer"},"evidence":{"head":"tête","tick":"tick","anchor":"ancre","coverage":"couverture","complete":"complète","partial":"partielle","unavailable":"indisponible"}},"history":{"title":"Historique Echo","empty":"Pas encore de preuve Echo","header":"# tick type statut preuve résumé"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Recherche","placeholder":"Filtrer les fichiers…","empty":"Aucun résultat","preview_label":"Aperçu","preview_loading":"Chargement de l’aperçu…","preview_binary":"Fichier binaire — aperçu indisponible","preview_unavailable":"Aperçu indisponible","preview_truncated":"Aperçu tronqué"}}; -export const it: TranslationSchema = {"footer":{"mode":{"browse":"sfoglia","insert":"inserisci","normal":"normale","preview":"anteprima","settings":"impostazioni","files":"file","graft":"graft","history":"cronologia"},"context":{"settings":"impostazioni","graft_empty":"apri un file per ispezionarlo","history_empty":"nessuna prova Echo ancora","history_count":"Prove Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k muovi","enter_change":"enter cambia","f2_close":"f2 chiudi","esc_close":"esc chiudi","j_k_scroll":"j/k scorri","f3_source":"f3 sorgente","f3_preview":"f3 anteprima","text_input":"testo","esc_normal":"esc normale","ctrl_s_save":"ctrl+s salva","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab focus","tab_indent":"tab rientra","ctrl_b_files":"ctrl+b file","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h cronologia","enter_open":"enter apri","backspace_up":"backspace su","ctrl_b_close":"ctrl+b chiudi","enter_jump":"enter vai","r_refresh":"r aggiorna","ctrl_g_close":"ctrl+g chiudi","ctrl_h_close":"ctrl+h chiudi","i_insert":"i inserisci","o_open_line":"o apri riga","cc_line":"cc riga","cw_word":"cw parola","ce_word_end":"ce fine parola","c0_start":"c0 inizio","c_end":"c$ fine","dd_line":"dd riga","dw_word":"dw parola","de_word_end":"de fine parola","d0_start":"d0 inizio","d_end":"d$ fine","yy_line":"yy riga","yw_word":"yw parola","ye_word_end":"ye fine parola","y0_start":"y0 inizio","y_end":"y$ fine","gg_top":"gg cima","esc_cancel":"esc annulla"},"evidence":{"head":"testa","tick":"tick","anchor":"ancora","coverage":"copertura","complete":"completa","partial":"parziale","unavailable":"non disponibile"}},"history":{"title":"Cronologia Echo","empty":"Nessuna prova Echo ancora","header":"# tick tipo stato prova riepilogo"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const it: TranslationSchema = {"footer":{"mode":{"browse":"sfoglia","insert":"inserisci","normal":"normale","preview":"anteprima","settings":"impostazioni","files":"file","graft":"graft","history":"cronologia"},"context":{"settings":"impostazioni","graft_empty":"apri un file per ispezionarlo","history_empty":"nessuna prova Echo ancora","history_count":"Prove Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k muovi","enter_change":"enter cambia","f2_close":"f2 chiudi","esc_close":"esc chiudi","j_k_scroll":"j/k scorri","f3_source":"f3 sorgente","f3_preview":"f3 anteprima","text_input":"testo","esc_normal":"esc normale","ctrl_s_save":"ctrl+s salva","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab focus","tab_indent":"tab rientra","ctrl_b_files":"ctrl+b file","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h cronologia","enter_open":"enter apri","backspace_up":"backspace su","ctrl_b_close":"ctrl+b chiudi","enter_jump":"enter vai","r_refresh":"r aggiorna","ctrl_g_close":"ctrl+g chiudi","ctrl_h_close":"ctrl+h chiudi","i_insert":"i inserisci","o_open_line":"o apri riga","cc_line":"cc riga","cw_word":"cw parola","ce_word_end":"ce fine parola","c0_start":"c0 inizio","c_end":"c$ fine","dd_line":"dd riga","dw_word":"dw parola","de_word_end":"de fine parola","d0_start":"d0 inizio","d_end":"d$ fine","yy_line":"yy riga","yw_word":"yw parola","ye_word_end":"ye fine parola","y0_start":"y0 inizio","y_end":"y$ fine","gg_top":"gg cima","esc_cancel":"esc annulla","type_filter":"digita filtro","arrows_select":"↑/↓ seleziona","slash_filter":"/ filtra"},"evidence":{"head":"testa","tick":"tick","anchor":"ancora","coverage":"copertura","complete":"completa","partial":"parziale","unavailable":"non disponibile"}},"history":{"title":"Cronologia Echo","empty":"Nessuna prova Echo ancora","header":"# tick tipo stato prova riepilogo"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Cerca","placeholder":"Filtra file…","empty":"Nessun risultato","preview_label":"Anteprima","preview_loading":"Caricamento anteprima…","preview_binary":"File binario — anteprima non disponibile","preview_unavailable":"Anteprima non disponibile","preview_truncated":"Anteprima troncata"}}; -export const de: TranslationSchema = {"footer":{"mode":{"browse":"durchsuchen","insert":"einfügen","normal":"normal","preview":"vorschau","settings":"einstellungen","files":"dateien","graft":"graft","history":"verlauf"},"context":{"settings":"einstellungen","graft_empty":"datei öffnen zum prüfen","history_empty":"noch keine Echo-Nachweise","history_count":"Echo-Nachweise: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k bewegen","enter_change":"enter ändern","f2_close":"f2 schließen","esc_close":"esc schließen","j_k_scroll":"j/k scrollen","f3_source":"f3 quelltext","f3_preview":"f3 vorschau","text_input":"texteingabe","esc_normal":"esc normal","ctrl_s_save":"ctrl+s speichern","ctrl_t_theme":"ctrl+t theme","tab_focus":"tab fokus","tab_indent":"tab einrücken","ctrl_b_files":"ctrl+b dateien","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h verlauf","enter_open":"enter öffnen","backspace_up":"backspace hoch","ctrl_b_close":"ctrl+b schließen","enter_jump":"enter springen","r_refresh":"r aktualisieren","ctrl_g_close":"ctrl+g schließen","ctrl_h_close":"ctrl+h schließen","i_insert":"i einfügen","o_open_line":"o zeile öffnen","cc_line":"cc zeile","cw_word":"cw wort","ce_word_end":"ce wortende","c0_start":"c0 anfang","c_end":"c$ ende","dd_line":"dd zeile","dw_word":"dw wort","de_word_end":"de wortende","d0_start":"d0 anfang","d_end":"d$ ende","yy_line":"yy zeile","yw_word":"yw wort","ye_word_end":"ye wortende","y0_start":"y0 anfang","y_end":"y$ ende","gg_top":"gg oben","esc_cancel":"esc abbrechen"},"evidence":{"head":"Kopf","tick":"Tick","anchor":"Anker","coverage":"Abdeckung","complete":"vollständig","partial":"teilweise","unavailable":"nicht verfügbar"}},"history":{"title":"Echo-Verlauf","empty":"Noch keine Echo-Nachweise","header":"# tick art status nachweis zusammenfassung"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const de: TranslationSchema = {"footer":{"mode":{"browse":"durchsuchen","insert":"einfügen","normal":"normal","preview":"vorschau","settings":"einstellungen","files":"dateien","graft":"graft","history":"verlauf"},"context":{"settings":"einstellungen","graft_empty":"datei öffnen zum prüfen","history_empty":"noch keine Echo-Nachweise","history_count":"Echo-Nachweise: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k bewegen","enter_change":"enter ändern","f2_close":"f2 schließen","esc_close":"esc schließen","j_k_scroll":"j/k scrollen","f3_source":"f3 quelltext","f3_preview":"f3 vorschau","text_input":"texteingabe","esc_normal":"esc normal","ctrl_s_save":"ctrl+s speichern","ctrl_t_theme":"ctrl+t theme","tab_focus":"tab fokus","tab_indent":"tab einrücken","ctrl_b_files":"ctrl+b dateien","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h verlauf","enter_open":"enter öffnen","backspace_up":"backspace hoch","ctrl_b_close":"ctrl+b schließen","enter_jump":"enter springen","r_refresh":"r aktualisieren","ctrl_g_close":"ctrl+g schließen","ctrl_h_close":"ctrl+h schließen","i_insert":"i einfügen","o_open_line":"o zeile öffnen","cc_line":"cc zeile","cw_word":"cw wort","ce_word_end":"ce wortende","c0_start":"c0 anfang","c_end":"c$ ende","dd_line":"dd zeile","dw_word":"dw wort","de_word_end":"de wortende","d0_start":"d0 anfang","d_end":"d$ ende","yy_line":"yy zeile","yw_word":"yw wort","ye_word_end":"ye wortende","y0_start":"y0 anfang","y_end":"y$ ende","gg_top":"gg oben","esc_cancel":"esc abbrechen","type_filter":"Filter eingeben","arrows_select":"↑/↓ auswählen","slash_filter":"/ filtern"},"evidence":{"head":"Kopf","tick":"Tick","anchor":"Anker","coverage":"Abdeckung","complete":"vollständig","partial":"teilweise","unavailable":"nicht verfügbar"}},"history":{"title":"Echo-Verlauf","empty":"Noch keine Echo-Nachweise","header":"# tick art status nachweis zusammenfassung"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Suche","placeholder":"Dateien filtern…","empty":"Keine Treffer","preview_label":"Vorschau","preview_loading":"Vorschau wird geladen…","preview_binary":"Binärdatei — keine Vorschau","preview_unavailable":"Vorschau nicht verfügbar","preview_truncated":"Vorschau gekürzt"}}; -export const es: TranslationSchema = {"footer":{"mode":{"browse":"explorar","insert":"insertar","normal":"normal","preview":"vista previa","settings":"ajustes","files":"archivos","graft":"graft","history":"historial"},"context":{"settings":"ajustes","graft_empty":"abre un archivo para inspeccionarlo","history_empty":"sin evidencia Echo todavía","history_count":"Evidencia Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k mover","enter_change":"enter cambiar","f2_close":"f2 cerrar","esc_close":"esc cerrar","j_k_scroll":"j/k desplazar","f3_source":"f3 fuente","f3_preview":"f3 vista previa","text_input":"texto","esc_normal":"esc normal","ctrl_s_save":"ctrl+s guardar","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab foco","tab_indent":"tab sangría","ctrl_b_files":"ctrl+b archivos","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historial","enter_open":"enter abrir","backspace_up":"backspace arriba","ctrl_b_close":"ctrl+b cerrar","enter_jump":"enter saltar","r_refresh":"r actualizar","ctrl_g_close":"ctrl+g cerrar","ctrl_h_close":"ctrl+h cerrar","i_insert":"i insertar","o_open_line":"o abrir línea","cc_line":"cc línea","cw_word":"cw palabra","ce_word_end":"ce fin palabra","c0_start":"c0 inicio","c_end":"c$ fin","dd_line":"dd línea","dw_word":"dw palabra","de_word_end":"de fin palabra","d0_start":"d0 inicio","d_end":"d$ fin","yy_line":"yy línea","yw_word":"yw palabra","ye_word_end":"ye fin palabra","y0_start":"y0 inicio","y_end":"y$ fin","gg_top":"gg arriba","esc_cancel":"esc cancelar"},"evidence":{"head":"cabeza","tick":"tick","anchor":"ancla","coverage":"cobertura","complete":"completa","partial":"parcial","unavailable":"no disponible"}},"history":{"title":"Historial Echo","empty":"Sin evidencia Echo todavía","header":"# tick tipo estado evidencia resumen"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const es: TranslationSchema = {"footer":{"mode":{"browse":"explorar","insert":"insertar","normal":"normal","preview":"vista previa","settings":"ajustes","files":"archivos","graft":"graft","history":"historial"},"context":{"settings":"ajustes","graft_empty":"abre un archivo para inspeccionarlo","history_empty":"sin evidencia Echo todavía","history_count":"Evidencia Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k mover","enter_change":"enter cambiar","f2_close":"f2 cerrar","esc_close":"esc cerrar","j_k_scroll":"j/k desplazar","f3_source":"f3 fuente","f3_preview":"f3 vista previa","text_input":"texto","esc_normal":"esc normal","ctrl_s_save":"ctrl+s guardar","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab foco","tab_indent":"tab sangría","ctrl_b_files":"ctrl+b archivos","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historial","enter_open":"enter abrir","backspace_up":"backspace arriba","ctrl_b_close":"ctrl+b cerrar","enter_jump":"enter saltar","r_refresh":"r actualizar","ctrl_g_close":"ctrl+g cerrar","ctrl_h_close":"ctrl+h cerrar","i_insert":"i insertar","o_open_line":"o abrir línea","cc_line":"cc línea","cw_word":"cw palabra","ce_word_end":"ce fin palabra","c0_start":"c0 inicio","c_end":"c$ fin","dd_line":"dd línea","dw_word":"dw palabra","de_word_end":"de fin palabra","d0_start":"d0 inicio","d_end":"d$ fin","yy_line":"yy línea","yw_word":"yw palabra","ye_word_end":"ye fin palabra","y0_start":"y0 inicio","y_end":"y$ fin","gg_top":"gg arriba","esc_cancel":"esc cancelar","type_filter":"escribir filtro","arrows_select":"↑/↓ seleccionar","slash_filter":"/ filtrar"},"evidence":{"head":"cabeza","tick":"tick","anchor":"ancla","coverage":"cobertura","complete":"completa","partial":"parcial","unavailable":"no disponible"}},"history":{"title":"Historial Echo","empty":"Sin evidencia Echo todavía","header":"# tick tipo estado evidencia resumen"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Buscar","placeholder":"Filtrar archivos…","empty":"Sin resultados","preview_label":"Vista previa","preview_loading":"Cargando vista previa…","preview_binary":"Archivo binario — sin vista previa","preview_unavailable":"Vista previa no disponible","preview_truncated":"Vista previa truncada"}}; -export const ko: TranslationSchema = {"footer":{"mode":{"browse":"탐색","insert":"삽입","normal":"일반","preview":"미리보기","settings":"설정","files":"파일","graft":"Graft","history":"기록"},"context":{"settings":"설정","graft_empty":"검사할 파일을 여세요","history_empty":"아직 Echo 증거 없음","history_count":"Echo 증거: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 이동","enter_change":"enter 변경","f2_close":"f2 닫기","esc_close":"esc 닫기","j_k_scroll":"j/k 스크롤","f3_source":"f3 소스","f3_preview":"f3 미리보기","text_input":"텍스트 입력","esc_normal":"esc 일반","ctrl_s_save":"ctrl+s 저장","ctrl_t_theme":"ctrl+t 테마","tab_focus":"tab 포커스","tab_indent":"tab 들여쓰기","ctrl_b_files":"ctrl+b 파일","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 기록","enter_open":"enter 열기","backspace_up":"backspace 위로","ctrl_b_close":"ctrl+b 닫기","enter_jump":"enter 이동","r_refresh":"r 새로고침","ctrl_g_close":"ctrl+g 닫기","ctrl_h_close":"ctrl+h 닫기","i_insert":"i 삽입","o_open_line":"o 줄 열기","cc_line":"cc 줄","cw_word":"cw 단어","ce_word_end":"ce 단어 끝","c0_start":"c0 시작","c_end":"c$ 끝","dd_line":"dd 줄","dw_word":"dw 단어","de_word_end":"de 단어 끝","d0_start":"d0 시작","d_end":"d$ 끝","yy_line":"yy 줄","yw_word":"yw 단어","ye_word_end":"ye 단어 끝","y0_start":"y0 시작","y_end":"y$ 끝","gg_top":"gg 맨 위","esc_cancel":"esc 취소"},"evidence":{"head":"헤드","tick":"틱","anchor":"앵커","coverage":"범위","complete":"완전","partial":"부분","unavailable":"사용 불가"}},"history":{"title":"Echo 기록","empty":"아직 Echo 증거 없음","header":"# tick 종류 상태 증거 요약"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const ko: TranslationSchema = {"footer":{"mode":{"browse":"탐색","insert":"삽입","normal":"일반","preview":"미리보기","settings":"설정","files":"파일","graft":"Graft","history":"기록"},"context":{"settings":"설정","graft_empty":"검사할 파일을 여세요","history_empty":"아직 Echo 증거 없음","history_count":"Echo 증거: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 이동","enter_change":"enter 변경","f2_close":"f2 닫기","esc_close":"esc 닫기","j_k_scroll":"j/k 스크롤","f3_source":"f3 소스","f3_preview":"f3 미리보기","text_input":"텍스트 입력","esc_normal":"esc 일반","ctrl_s_save":"ctrl+s 저장","ctrl_t_theme":"ctrl+t 테마","tab_focus":"tab 포커스","tab_indent":"tab 들여쓰기","ctrl_b_files":"ctrl+b 파일","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 기록","enter_open":"enter 열기","backspace_up":"backspace 위로","ctrl_b_close":"ctrl+b 닫기","enter_jump":"enter 이동","r_refresh":"r 새로고침","ctrl_g_close":"ctrl+g 닫기","ctrl_h_close":"ctrl+h 닫기","i_insert":"i 삽입","o_open_line":"o 줄 열기","cc_line":"cc 줄","cw_word":"cw 단어","ce_word_end":"ce 단어 끝","c0_start":"c0 시작","c_end":"c$ 끝","dd_line":"dd 줄","dw_word":"dw 단어","de_word_end":"de 단어 끝","d0_start":"d0 시작","d_end":"d$ 끝","yy_line":"yy 줄","yw_word":"yw 단어","ye_word_end":"ye 단어 끝","y0_start":"y0 시작","y_end":"y$ 끝","gg_top":"gg 맨 위","esc_cancel":"esc 취소","type_filter":"필터 입력","arrows_select":"↑/↓ 선택","slash_filter":"/ 필터"},"evidence":{"head":"헤드","tick":"틱","anchor":"앵커","coverage":"범위","complete":"완전","partial":"부분","unavailable":"사용 불가"}},"history":{"title":"Echo 기록","empty":"아직 Echo 증거 없음","header":"# tick 종류 상태 증거 요약"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"검색","placeholder":"파일 필터…","empty":"일치 항목 없음","preview_label":"미리 보기","preview_loading":"미리 보기 불러오는 중…","preview_binary":"바이너리 파일 — 미리 보기 불가","preview_unavailable":"미리 보기 불가","preview_truncated":"미리 보기 생략됨"}}; -export const ja: TranslationSchema = {"footer":{"mode":{"browse":"参照","insert":"挿入","normal":"通常","preview":"プレビュー","settings":"設定","files":"ファイル","graft":"Graft","history":"履歴"},"context":{"settings":"設定","graft_empty":"検査するファイルを開く","history_empty":"Echo 証拠はまだありません","history_count":"Echo 証拠: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 移動","enter_change":"enter 変更","f2_close":"f2 閉じる","esc_close":"esc 閉じる","j_k_scroll":"j/k スクロール","f3_source":"f3 ソース","f3_preview":"f3 プレビュー","text_input":"テキスト入力","esc_normal":"esc 通常","ctrl_s_save":"ctrl+s 保存","ctrl_t_theme":"ctrl+t テーマ","tab_focus":"tab フォーカス","tab_indent":"tab インデント","ctrl_b_files":"ctrl+b ファイル","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 履歴","enter_open":"enter 開く","backspace_up":"backspace 上へ","ctrl_b_close":"ctrl+b 閉じる","enter_jump":"enter 移動","r_refresh":"r 更新","ctrl_g_close":"ctrl+g 閉じる","ctrl_h_close":"ctrl+h 閉じる","i_insert":"i 挿入","o_open_line":"o 行を開く","cc_line":"cc 行","cw_word":"cw 単語","ce_word_end":"ce 単語末","c0_start":"c0 先頭","c_end":"c$ 末尾","dd_line":"dd 行","dw_word":"dw 単語","de_word_end":"de 単語末","d0_start":"d0 先頭","d_end":"d$ 末尾","yy_line":"yy 行","yw_word":"yw 単語","ye_word_end":"ye 単語末","y0_start":"y0 先頭","y_end":"y$ 末尾","gg_top":"gg 先頭","esc_cancel":"esc 取消"},"evidence":{"head":"ヘッド","tick":"ティック","anchor":"アンカー","coverage":"範囲","complete":"完全","partial":"部分","unavailable":"利用不可"}},"history":{"title":"Echo 履歴","empty":"Echo 証拠はまだありません","header":"# tick 種類 状態 証拠 概要"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const ja: TranslationSchema = {"footer":{"mode":{"browse":"参照","insert":"挿入","normal":"通常","preview":"プレビュー","settings":"設定","files":"ファイル","graft":"Graft","history":"履歴"},"context":{"settings":"設定","graft_empty":"検査するファイルを開く","history_empty":"Echo 証拠はまだありません","history_count":"Echo 証拠: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 移動","enter_change":"enter 変更","f2_close":"f2 閉じる","esc_close":"esc 閉じる","j_k_scroll":"j/k スクロール","f3_source":"f3 ソース","f3_preview":"f3 プレビュー","text_input":"テキスト入力","esc_normal":"esc 通常","ctrl_s_save":"ctrl+s 保存","ctrl_t_theme":"ctrl+t テーマ","tab_focus":"tab フォーカス","tab_indent":"tab インデント","ctrl_b_files":"ctrl+b ファイル","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 履歴","enter_open":"enter 開く","backspace_up":"backspace 上へ","ctrl_b_close":"ctrl+b 閉じる","enter_jump":"enter 移動","r_refresh":"r 更新","ctrl_g_close":"ctrl+g 閉じる","ctrl_h_close":"ctrl+h 閉じる","i_insert":"i 挿入","o_open_line":"o 行を開く","cc_line":"cc 行","cw_word":"cw 単語","ce_word_end":"ce 単語末","c0_start":"c0 先頭","c_end":"c$ 末尾","dd_line":"dd 行","dw_word":"dw 単語","de_word_end":"de 単語末","d0_start":"d0 先頭","d_end":"d$ 末尾","yy_line":"yy 行","yw_word":"yw 単語","ye_word_end":"ye 単語末","y0_start":"y0 先頭","y_end":"y$ 末尾","gg_top":"gg 先頭","esc_cancel":"esc 取消","type_filter":"絞り込み入力","arrows_select":"↑/↓ 選択","slash_filter":"/ 絞り込み"},"evidence":{"head":"ヘッド","tick":"ティック","anchor":"アンカー","coverage":"範囲","complete":"完全","partial":"部分","unavailable":"利用不可"}},"history":{"title":"Echo 履歴","empty":"Echo 証拠はまだありません","header":"# tick 種類 状態 証拠 概要"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"検索","placeholder":"ファイルを絞り込む…","empty":"一致なし","preview_label":"プレビュー","preview_loading":"プレビューを読み込み中…","preview_binary":"バイナリファイル — プレビュー不可","preview_unavailable":"プレビュー不可","preview_truncated":"プレビューを省略"}}; -export const zhHans: TranslationSchema = {"footer":{"mode":{"browse":"浏览","insert":"插入","normal":"普通","preview":"预览","settings":"设置","files":"文件","graft":"Graft","history":"历史"},"context":{"settings":"设置","graft_empty":"打开文件以检查","history_empty":"还没有 Echo 证据","history_count":"Echo 证据: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 移动","enter_change":"enter 更改","f2_close":"f2 关闭","esc_close":"esc 关闭","j_k_scroll":"j/k 滚动","f3_source":"f3 源码","f3_preview":"f3 预览","text_input":"文本输入","esc_normal":"esc 普通","ctrl_s_save":"ctrl+s 保存","ctrl_t_theme":"ctrl+t 主题","tab_focus":"tab 焦点","tab_indent":"tab 缩进","ctrl_b_files":"ctrl+b 文件","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 历史","enter_open":"enter 打开","backspace_up":"backspace 上一级","ctrl_b_close":"ctrl+b 关闭","enter_jump":"enter 跳转","r_refresh":"r 刷新","ctrl_g_close":"ctrl+g 关闭","ctrl_h_close":"ctrl+h 关闭","i_insert":"i 插入","o_open_line":"o 打开行","cc_line":"cc 行","cw_word":"cw 词","ce_word_end":"ce 词尾","c0_start":"c0 开头","c_end":"c$ 结尾","dd_line":"dd 行","dw_word":"dw 词","de_word_end":"de 词尾","d0_start":"d0 开头","d_end":"d$ 结尾","yy_line":"yy 行","yw_word":"yw 词","ye_word_end":"ye 词尾","y0_start":"y0 开头","y_end":"y$ 结尾","gg_top":"gg 顶部","esc_cancel":"esc 取消"},"evidence":{"head":"头","tick":"时刻","anchor":"锚点","coverage":"覆盖","complete":"完整","partial":"部分","unavailable":"不可用"}},"history":{"title":"Echo 历史","empty":"还没有 Echo 证据","header":"# tick 类型 状态 证据 摘要"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const zhHans: TranslationSchema = {"footer":{"mode":{"browse":"浏览","insert":"插入","normal":"普通","preview":"预览","settings":"设置","files":"文件","graft":"Graft","history":"历史"},"context":{"settings":"设置","graft_empty":"打开文件以检查","history_empty":"还没有 Echo 证据","history_count":"Echo 证据: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k 移动","enter_change":"enter 更改","f2_close":"f2 关闭","esc_close":"esc 关闭","j_k_scroll":"j/k 滚动","f3_source":"f3 源码","f3_preview":"f3 预览","text_input":"文本输入","esc_normal":"esc 普通","ctrl_s_save":"ctrl+s 保存","ctrl_t_theme":"ctrl+t 主题","tab_focus":"tab 焦点","tab_indent":"tab 缩进","ctrl_b_files":"ctrl+b 文件","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h 历史","enter_open":"enter 打开","backspace_up":"backspace 上一级","ctrl_b_close":"ctrl+b 关闭","enter_jump":"enter 跳转","r_refresh":"r 刷新","ctrl_g_close":"ctrl+g 关闭","ctrl_h_close":"ctrl+h 关闭","i_insert":"i 插入","o_open_line":"o 打开行","cc_line":"cc 行","cw_word":"cw 词","ce_word_end":"ce 词尾","c0_start":"c0 开头","c_end":"c$ 结尾","dd_line":"dd 行","dw_word":"dw 词","de_word_end":"de 词尾","d0_start":"d0 开头","d_end":"d$ 结尾","yy_line":"yy 行","yw_word":"yw 词","ye_word_end":"ye 词尾","y0_start":"y0 开头","y_end":"y$ 结尾","gg_top":"gg 顶部","esc_cancel":"esc 取消","type_filter":"输入筛选","arrows_select":"↑/↓ 选择","slash_filter":"/ 筛选"},"evidence":{"head":"头","tick":"时刻","anchor":"锚点","coverage":"覆盖","complete":"完整","partial":"部分","unavailable":"不可用"}},"history":{"title":"Echo 历史","empty":"还没有 Echo 证据","header":"# tick 类型 状态 证据 摘要"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"搜索","placeholder":"筛选文件…","empty":"无匹配项","preview_label":"预览","preview_loading":"正在加载预览…","preview_binary":"二进制文件 — 无法预览","preview_unavailable":"无法预览","preview_truncated":"预览已截断"}}; -export const ptBR: TranslationSchema = {"footer":{"mode":{"browse":"navegar","insert":"inserir","normal":"normal","preview":"prévia","settings":"configurações","files":"arquivos","graft":"graft","history":"histórico"},"context":{"settings":"configurações","graft_empty":"abra um arquivo para inspecionar","history_empty":"nenhuma evidência Echo ainda","history_count":"Evidências Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k mover","enter_change":"enter alterar","f2_close":"f2 fechar","esc_close":"esc fechar","j_k_scroll":"j/k rolar","f3_source":"f3 código","f3_preview":"f3 prévia","text_input":"entrada de texto","esc_normal":"esc normal","ctrl_s_save":"ctrl+s salvar","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab foco","tab_indent":"tab recuo","ctrl_b_files":"ctrl+b arquivos","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h histórico","enter_open":"enter abrir","backspace_up":"backspace subir","ctrl_b_close":"ctrl+b fechar","enter_jump":"enter ir","r_refresh":"r atualizar","ctrl_g_close":"ctrl+g fechar","ctrl_h_close":"ctrl+h fechar","i_insert":"i inserir","o_open_line":"o abrir linha","cc_line":"cc linha","cw_word":"cw palavra","ce_word_end":"ce fim palavra","c0_start":"c0 início","c_end":"c$ fim","dd_line":"dd linha","dw_word":"dw palavra","de_word_end":"de fim palavra","d0_start":"d0 início","d_end":"d$ fim","yy_line":"yy linha","yw_word":"yw palavra","ye_word_end":"ye fim palavra","y0_start":"y0 início","y_end":"y$ fim","gg_top":"gg topo","esc_cancel":"esc cancelar"},"evidence":{"head":"cabeça","tick":"tick","anchor":"âncora","coverage":"cobertura","complete":"completa","partial":"parcial","unavailable":"indisponível"}},"history":{"title":"Histórico Echo","empty":"Nenhuma evidência Echo ainda","header":"# tick tipo status evidência resumo"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const ptBR: TranslationSchema = {"footer":{"mode":{"browse":"navegar","insert":"inserir","normal":"normal","preview":"prévia","settings":"configurações","files":"arquivos","graft":"graft","history":"histórico"},"context":{"settings":"configurações","graft_empty":"abra um arquivo para inspecionar","history_empty":"nenhuma evidência Echo ainda","history_count":"Evidências Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k mover","enter_change":"enter alterar","f2_close":"f2 fechar","esc_close":"esc fechar","j_k_scroll":"j/k rolar","f3_source":"f3 código","f3_preview":"f3 prévia","text_input":"entrada de texto","esc_normal":"esc normal","ctrl_s_save":"ctrl+s salvar","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab foco","tab_indent":"tab recuo","ctrl_b_files":"ctrl+b arquivos","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h histórico","enter_open":"enter abrir","backspace_up":"backspace subir","ctrl_b_close":"ctrl+b fechar","enter_jump":"enter ir","r_refresh":"r atualizar","ctrl_g_close":"ctrl+g fechar","ctrl_h_close":"ctrl+h fechar","i_insert":"i inserir","o_open_line":"o abrir linha","cc_line":"cc linha","cw_word":"cw palavra","ce_word_end":"ce fim palavra","c0_start":"c0 início","c_end":"c$ fim","dd_line":"dd linha","dw_word":"dw palavra","de_word_end":"de fim palavra","d0_start":"d0 início","d_end":"d$ fim","yy_line":"yy linha","yw_word":"yw palavra","ye_word_end":"ye fim palavra","y0_start":"y0 início","y_end":"y$ fim","gg_top":"gg topo","esc_cancel":"esc cancelar","type_filter":"digitar filtro","arrows_select":"↑/↓ selecionar","slash_filter":"/ filtrar"},"evidence":{"head":"cabeça","tick":"tick","anchor":"âncora","coverage":"cobertura","complete":"completa","partial":"parcial","unavailable":"indisponível"}},"history":{"title":"Histórico Echo","empty":"Nenhuma evidência Echo ainda","header":"# tick tipo status evidência resumo"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Buscar","placeholder":"Filtrar arquivos…","empty":"Nenhum resultado","preview_label":"Prévia","preview_loading":"Carregando prévia…","preview_binary":"Arquivo binário — sem prévia","preview_unavailable":"Prévia indisponível","preview_truncated":"Prévia truncada"}}; -export const ru: TranslationSchema = {"footer":{"mode":{"browse":"обзор","insert":"вставка","normal":"обычный","preview":"просмотр","settings":"настройки","files":"файлы","graft":"graft","history":"история"},"context":{"settings":"настройки","graft_empty":"откройте файл для проверки","history_empty":"пока нет доказательств Echo","history_count":"Доказательства Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k переместить","enter_change":"enter изменить","f2_close":"f2 закрыть","esc_close":"esc закрыть","j_k_scroll":"j/k прокрутка","f3_source":"f3 исходник","f3_preview":"f3 просмотр","text_input":"ввод текста","esc_normal":"esc обычный","ctrl_s_save":"ctrl+s сохранить","ctrl_t_theme":"ctrl+t тема","tab_focus":"tab фокус","tab_indent":"tab отступ","ctrl_b_files":"ctrl+b файлы","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h история","enter_open":"enter открыть","backspace_up":"backspace вверх","ctrl_b_close":"ctrl+b закрыть","enter_jump":"enter перейти","r_refresh":"r обновить","ctrl_g_close":"ctrl+g закрыть","ctrl_h_close":"ctrl+h закрыть","i_insert":"i вставка","o_open_line":"o открыть строку","cc_line":"cc строка","cw_word":"cw слово","ce_word_end":"ce конец слова","c0_start":"c0 начало","c_end":"c$ конец","dd_line":"dd строка","dw_word":"dw слово","de_word_end":"de конец слова","d0_start":"d0 начало","d_end":"d$ конец","yy_line":"yy строка","yw_word":"yw слово","ye_word_end":"ye конец слова","y0_start":"y0 начало","y_end":"y$ конец","gg_top":"gg верх","esc_cancel":"esc отмена"},"evidence":{"head":"вершина","tick":"тик","anchor":"якорь","coverage":"покрытие","complete":"полное","partial":"частичное","unavailable":"недоступно"}},"history":{"title":"История Echo","empty":"Пока нет доказательств Echo","header":"# tick тип статус доказательство сводка"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const ru: TranslationSchema = {"footer":{"mode":{"browse":"обзор","insert":"вставка","normal":"обычный","preview":"просмотр","settings":"настройки","files":"файлы","graft":"graft","history":"история"},"context":{"settings":"настройки","graft_empty":"откройте файл для проверки","history_empty":"пока нет доказательств Echo","history_count":"Доказательства Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k переместить","enter_change":"enter изменить","f2_close":"f2 закрыть","esc_close":"esc закрыть","j_k_scroll":"j/k прокрутка","f3_source":"f3 исходник","f3_preview":"f3 просмотр","text_input":"ввод текста","esc_normal":"esc обычный","ctrl_s_save":"ctrl+s сохранить","ctrl_t_theme":"ctrl+t тема","tab_focus":"tab фокус","tab_indent":"tab отступ","ctrl_b_files":"ctrl+b файлы","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h история","enter_open":"enter открыть","backspace_up":"backspace вверх","ctrl_b_close":"ctrl+b закрыть","enter_jump":"enter перейти","r_refresh":"r обновить","ctrl_g_close":"ctrl+g закрыть","ctrl_h_close":"ctrl+h закрыть","i_insert":"i вставка","o_open_line":"o открыть строку","cc_line":"cc строка","cw_word":"cw слово","ce_word_end":"ce конец слова","c0_start":"c0 начало","c_end":"c$ конец","dd_line":"dd строка","dw_word":"dw слово","de_word_end":"de конец слова","d0_start":"d0 начало","d_end":"d$ конец","yy_line":"yy строка","yw_word":"yw слово","ye_word_end":"ye конец слова","y0_start":"y0 начало","y_end":"y$ конец","gg_top":"gg верх","esc_cancel":"esc отмена","type_filter":"ввод фильтра","arrows_select":"↑/↓ выбор","slash_filter":"/ фильтр"},"evidence":{"head":"вершина","tick":"тик","anchor":"якорь","coverage":"покрытие","complete":"полное","partial":"частичное","unavailable":"недоступно"}},"history":{"title":"История Echo","empty":"Пока нет доказательств Echo","header":"# tick тип статус доказательство сводка"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Поиск","placeholder":"Фильтр файлов…","empty":"Нет совпадений","preview_label":"Просмотр","preview_loading":"Загрузка просмотра…","preview_binary":"Двоичный файл — просмотр недоступен","preview_unavailable":"Просмотр недоступен","preview_truncated":"Просмотр сокращён"}}; -export const ar: TranslationSchema = {"footer":{"mode":{"browse":"تصفح","insert":"إدراج","normal":"عادي","preview":"معاينة","settings":"إعدادات","files":"ملفات","graft":"Graft","history":"السجل"},"context":{"settings":"إعدادات","graft_empty":"افتح ملفا لفحصه","history_empty":"لا توجد أدلة Echo بعد","history_count":"أدلة Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k تحريك","enter_change":"enter تغيير","f2_close":"f2 إغلاق","esc_close":"esc إغلاق","j_k_scroll":"j/k تمرير","f3_source":"f3 المصدر","f3_preview":"f3 معاينة","text_input":"إدخال نص","esc_normal":"esc عادي","ctrl_s_save":"ctrl+s حفظ","ctrl_t_theme":"ctrl+t سمة","tab_focus":"tab تركيز","tab_indent":"tab إزاحة","ctrl_b_files":"ctrl+b ملفات","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h سجل","enter_open":"enter فتح","backspace_up":"backspace أعلى","ctrl_b_close":"ctrl+b إغلاق","enter_jump":"enter انتقال","r_refresh":"r تحديث","ctrl_g_close":"ctrl+g إغلاق","ctrl_h_close":"ctrl+h إغلاق","i_insert":"i إدراج","o_open_line":"o فتح سطر","cc_line":"cc سطر","cw_word":"cw كلمة","ce_word_end":"ce نهاية كلمة","c0_start":"c0 بداية","c_end":"c$ نهاية","dd_line":"dd سطر","dw_word":"dw كلمة","de_word_end":"de نهاية كلمة","d0_start":"d0 بداية","d_end":"d$ نهاية","yy_line":"yy سطر","yw_word":"yw كلمة","ye_word_end":"ye نهاية كلمة","y0_start":"y0 بداية","y_end":"y$ نهاية","gg_top":"gg أعلى","esc_cancel":"esc إلغاء"},"evidence":{"head":"الرأس","tick":"النبضة","anchor":"المرساة","coverage":"التغطية","complete":"كاملة","partial":"جزئية","unavailable":"غير متاحة"}},"history":{"title":"سجل Echo","empty":"لا توجد أدلة Echo بعد","header":"# tick النوع الحالة الدليل الملخص"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const ar: TranslationSchema = {"footer":{"mode":{"browse":"تصفح","insert":"إدراج","normal":"عادي","preview":"معاينة","settings":"إعدادات","files":"ملفات","graft":"Graft","history":"السجل"},"context":{"settings":"إعدادات","graft_empty":"افتح ملفا لفحصه","history_empty":"لا توجد أدلة Echo بعد","history_count":"أدلة Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k تحريك","enter_change":"enter تغيير","f2_close":"f2 إغلاق","esc_close":"esc إغلاق","j_k_scroll":"j/k تمرير","f3_source":"f3 المصدر","f3_preview":"f3 معاينة","text_input":"إدخال نص","esc_normal":"esc عادي","ctrl_s_save":"ctrl+s حفظ","ctrl_t_theme":"ctrl+t سمة","tab_focus":"tab تركيز","tab_indent":"tab إزاحة","ctrl_b_files":"ctrl+b ملفات","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h سجل","enter_open":"enter فتح","backspace_up":"backspace أعلى","ctrl_b_close":"ctrl+b إغلاق","enter_jump":"enter انتقال","r_refresh":"r تحديث","ctrl_g_close":"ctrl+g إغلاق","ctrl_h_close":"ctrl+h إغلاق","i_insert":"i إدراج","o_open_line":"o فتح سطر","cc_line":"cc سطر","cw_word":"cw كلمة","ce_word_end":"ce نهاية كلمة","c0_start":"c0 بداية","c_end":"c$ نهاية","dd_line":"dd سطر","dw_word":"dw كلمة","de_word_end":"de نهاية كلمة","d0_start":"d0 بداية","d_end":"d$ نهاية","yy_line":"yy سطر","yw_word":"yw كلمة","ye_word_end":"ye نهاية كلمة","y0_start":"y0 بداية","y_end":"y$ نهاية","gg_top":"gg أعلى","esc_cancel":"esc إلغاء","type_filter":"اكتب للتصفية","arrows_select":"↑/↓ تحديد","slash_filter":"/ تصفية"},"evidence":{"head":"الرأس","tick":"النبضة","anchor":"المرساة","coverage":"التغطية","complete":"كاملة","partial":"جزئية","unavailable":"غير متاحة"}},"history":{"title":"سجل Echo","empty":"لا توجد أدلة Echo بعد","header":"# tick النوع الحالة الدليل الملخص"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"بحث","placeholder":"تصفية الملفات…","empty":"لا نتائج","preview_label":"معاينة","preview_loading":"جارٍ تحميل المعاينة…","preview_binary":"ملف ثنائي — المعاينة غير متاحة","preview_unavailable":"المعاينة غير متاحة","preview_truncated":"المعاينة مختصرة"}}; -export const id: TranslationSchema = {"footer":{"mode":{"browse":"jelajah","insert":"sisip","normal":"normal","preview":"pratinjau","settings":"pengaturan","files":"berkas","graft":"graft","history":"riwayat"},"context":{"settings":"pengaturan","graft_empty":"buka berkas untuk diperiksa","history_empty":"belum ada bukti Echo","history_count":"Bukti Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k pindah","enter_change":"enter ubah","f2_close":"f2 tutup","esc_close":"esc tutup","j_k_scroll":"j/k gulir","f3_source":"f3 sumber","f3_preview":"f3 pratinjau","text_input":"input teks","esc_normal":"esc normal","ctrl_s_save":"ctrl+s simpan","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab fokus","tab_indent":"tab inden","ctrl_b_files":"ctrl+b berkas","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h riwayat","enter_open":"enter buka","backspace_up":"backspace naik","ctrl_b_close":"ctrl+b tutup","enter_jump":"enter lompat","r_refresh":"r segarkan","ctrl_g_close":"ctrl+g tutup","ctrl_h_close":"ctrl+h tutup","i_insert":"i sisip","o_open_line":"o buka baris","cc_line":"cc baris","cw_word":"cw kata","ce_word_end":"ce akhir kata","c0_start":"c0 awal","c_end":"c$ akhir","dd_line":"dd baris","dw_word":"dw kata","de_word_end":"de akhir kata","d0_start":"d0 awal","d_end":"d$ akhir","yy_line":"yy baris","yw_word":"yw kata","ye_word_end":"ye akhir kata","y0_start":"y0 awal","y_end":"y$ akhir","gg_top":"gg atas","esc_cancel":"esc batal"},"evidence":{"head":"kepala","tick":"tick","anchor":"jangkar","coverage":"cakupan","complete":"lengkap","partial":"sebagian","unavailable":"tidak tersedia"}},"history":{"title":"Riwayat Echo","empty":"Belum ada bukti Echo","header":"# tick jenis status bukti ringkasan"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const id: TranslationSchema = {"footer":{"mode":{"browse":"jelajah","insert":"sisip","normal":"normal","preview":"pratinjau","settings":"pengaturan","files":"berkas","graft":"graft","history":"riwayat"},"context":{"settings":"pengaturan","graft_empty":"buka berkas untuk diperiksa","history_empty":"belum ada bukti Echo","history_count":"Bukti Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k pindah","enter_change":"enter ubah","f2_close":"f2 tutup","esc_close":"esc tutup","j_k_scroll":"j/k gulir","f3_source":"f3 sumber","f3_preview":"f3 pratinjau","text_input":"input teks","esc_normal":"esc normal","ctrl_s_save":"ctrl+s simpan","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab fokus","tab_indent":"tab inden","ctrl_b_files":"ctrl+b berkas","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h riwayat","enter_open":"enter buka","backspace_up":"backspace naik","ctrl_b_close":"ctrl+b tutup","enter_jump":"enter lompat","r_refresh":"r segarkan","ctrl_g_close":"ctrl+g tutup","ctrl_h_close":"ctrl+h tutup","i_insert":"i sisip","o_open_line":"o buka baris","cc_line":"cc baris","cw_word":"cw kata","ce_word_end":"ce akhir kata","c0_start":"c0 awal","c_end":"c$ akhir","dd_line":"dd baris","dw_word":"dw kata","de_word_end":"de akhir kata","d0_start":"d0 awal","d_end":"d$ akhir","yy_line":"yy baris","yw_word":"yw kata","ye_word_end":"ye akhir kata","y0_start":"y0 awal","y_end":"y$ akhir","gg_top":"gg atas","esc_cancel":"esc batal","type_filter":"ketik filter","arrows_select":"↑/↓ pilih","slash_filter":"/ filter"},"evidence":{"head":"kepala","tick":"tick","anchor":"jangkar","coverage":"cakupan","complete":"lengkap","partial":"sebagian","unavailable":"tidak tersedia"}},"history":{"title":"Riwayat Echo","empty":"Belum ada bukti Echo","header":"# tick jenis status bukti ringkasan"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Cari","placeholder":"Filter berkas…","empty":"Tidak ada hasil","preview_label":"Pratinjau","preview_loading":"Memuat pratinjau…","preview_binary":"Berkas biner — pratinjau tidak tersedia","preview_unavailable":"Pratinjau tidak tersedia","preview_truncated":"Pratinjau dipotong"}}; -export const tr: TranslationSchema = {"footer":{"mode":{"browse":"gözat","insert":"ekle","normal":"normal","preview":"önizleme","settings":"ayarlar","files":"dosyalar","graft":"graft","history":"geçmiş"},"context":{"settings":"ayarlar","graft_empty":"incelemek için dosya aç","history_empty":"henüz Echo kanıtı yok","history_count":"Echo kanıtı: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k taşı","enter_change":"enter değiştir","f2_close":"f2 kapat","esc_close":"esc kapat","j_k_scroll":"j/k kaydır","f3_source":"f3 kaynak","f3_preview":"f3 önizleme","text_input":"metin girişi","esc_normal":"esc normal","ctrl_s_save":"ctrl+s kaydet","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab odak","tab_indent":"tab girinti","ctrl_b_files":"ctrl+b dosyalar","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h geçmiş","enter_open":"enter aç","backspace_up":"backspace yukarı","ctrl_b_close":"ctrl+b kapat","enter_jump":"enter git","r_refresh":"r yenile","ctrl_g_close":"ctrl+g kapat","ctrl_h_close":"ctrl+h kapat","i_insert":"i ekle","o_open_line":"o satır aç","cc_line":"cc satır","cw_word":"cw kelime","ce_word_end":"ce kelime sonu","c0_start":"c0 başlangıç","c_end":"c$ son","dd_line":"dd satır","dw_word":"dw kelime","de_word_end":"de kelime sonu","d0_start":"d0 başlangıç","d_end":"d$ son","yy_line":"yy satır","yw_word":"yw kelime","ye_word_end":"ye kelime sonu","y0_start":"y0 başlangıç","y_end":"y$ son","gg_top":"gg üst","esc_cancel":"esc iptal"},"evidence":{"head":"baş","tick":"tik","anchor":"çapa","coverage":"kapsam","complete":"tam","partial":"kısmi","unavailable":"kullanılamıyor"}},"history":{"title":"Echo Geçmişi","empty":"Henüz Echo kanıtı yok","header":"# tick tür durum kanıt özet"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const tr: TranslationSchema = {"footer":{"mode":{"browse":"gözat","insert":"ekle","normal":"normal","preview":"önizleme","settings":"ayarlar","files":"dosyalar","graft":"graft","history":"geçmiş"},"context":{"settings":"ayarlar","graft_empty":"incelemek için dosya aç","history_empty":"henüz Echo kanıtı yok","history_count":"Echo kanıtı: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k taşı","enter_change":"enter değiştir","f2_close":"f2 kapat","esc_close":"esc kapat","j_k_scroll":"j/k kaydır","f3_source":"f3 kaynak","f3_preview":"f3 önizleme","text_input":"metin girişi","esc_normal":"esc normal","ctrl_s_save":"ctrl+s kaydet","ctrl_t_theme":"ctrl+t tema","tab_focus":"tab odak","tab_indent":"tab girinti","ctrl_b_files":"ctrl+b dosyalar","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h geçmiş","enter_open":"enter aç","backspace_up":"backspace yukarı","ctrl_b_close":"ctrl+b kapat","enter_jump":"enter git","r_refresh":"r yenile","ctrl_g_close":"ctrl+g kapat","ctrl_h_close":"ctrl+h kapat","i_insert":"i ekle","o_open_line":"o satır aç","cc_line":"cc satır","cw_word":"cw kelime","ce_word_end":"ce kelime sonu","c0_start":"c0 başlangıç","c_end":"c$ son","dd_line":"dd satır","dw_word":"dw kelime","de_word_end":"de kelime sonu","d0_start":"d0 başlangıç","d_end":"d$ son","yy_line":"yy satır","yw_word":"yw kelime","ye_word_end":"ye kelime sonu","y0_start":"y0 başlangıç","y_end":"y$ son","gg_top":"gg üst","esc_cancel":"esc iptal","type_filter":"filtre yaz","arrows_select":"↑/↓ seç","slash_filter":"/ filtre"},"evidence":{"head":"baş","tick":"tik","anchor":"çapa","coverage":"kapsam","complete":"tam","partial":"kısmi","unavailable":"kullanılamıyor"}},"history":{"title":"Echo Geçmişi","empty":"Henüz Echo kanıtı yok","header":"# tick tür durum kanıt özet"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Ara","placeholder":"Dosyaları filtrele…","empty":"Eşleşme yok","preview_label":"Önizleme","preview_loading":"Önizleme yükleniyor…","preview_binary":"İkili dosya — önizleme yok","preview_unavailable":"Önizleme kullanılamıyor","preview_truncated":"Önizleme kısaltıldı"}}; -export const hi: TranslationSchema = {"footer":{"mode":{"browse":"ब्राउज़","insert":"सम्मिलित","normal":"सामान्य","preview":"पूर्वावलोकन","settings":"सेटिंग","files":"फाइलें","graft":"Graft","history":"इतिहास"},"context":{"settings":"सेटिंग","graft_empty":"जांचने के लिए फाइल खोलें","history_empty":"अभी कोई Echo प्रमाण नहीं","history_count":"Echo प्रमाण: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k चलें","enter_change":"enter बदलें","f2_close":"f2 बंद","esc_close":"esc बंद","j_k_scroll":"j/k स्क्रोल","f3_source":"f3 स्रोत","f3_preview":"f3 पूर्वावलोकन","text_input":"पाठ इनपुट","esc_normal":"esc सामान्य","ctrl_s_save":"ctrl+s सहेजें","ctrl_t_theme":"ctrl+t थीम","tab_focus":"tab फोकस","tab_indent":"tab इंडेंट","ctrl_b_files":"ctrl+b फाइलें","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h इतिहास","enter_open":"enter खोलें","backspace_up":"backspace ऊपर","ctrl_b_close":"ctrl+b बंद","enter_jump":"enter जाएं","r_refresh":"r ताज़ा","ctrl_g_close":"ctrl+g बंद","ctrl_h_close":"ctrl+h बंद","i_insert":"i सम्मिलित","o_open_line":"o पंक्ति खोलें","cc_line":"cc पंक्ति","cw_word":"cw शब्द","ce_word_end":"ce शब्द अंत","c0_start":"c0 शुरुआत","c_end":"c$ अंत","dd_line":"dd पंक्ति","dw_word":"dw शब्द","de_word_end":"de शब्द अंत","d0_start":"d0 शुरुआत","d_end":"d$ अंत","yy_line":"yy पंक्ति","yw_word":"yw शब्द","ye_word_end":"ye शब्द अंत","y0_start":"y0 शुरुआत","y_end":"y$ अंत","gg_top":"gg ऊपर","esc_cancel":"esc रद्द"},"evidence":{"head":"शीर्ष","tick":"टिक","anchor":"एंकर","coverage":"कवरेज","complete":"पूर्ण","partial":"आंशिक","unavailable":"अनुपलब्ध"}},"history":{"title":"Echo इतिहास","empty":"अभी कोई Echo प्रमाण नहीं","header":"# tick प्रकार स्थिति प्रमाण सारांश"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const hi: TranslationSchema = {"footer":{"mode":{"browse":"ब्राउज़","insert":"सम्मिलित","normal":"सामान्य","preview":"पूर्वावलोकन","settings":"सेटिंग","files":"फाइलें","graft":"Graft","history":"इतिहास"},"context":{"settings":"सेटिंग","graft_empty":"जांचने के लिए फाइल खोलें","history_empty":"अभी कोई Echo प्रमाण नहीं","history_count":"Echo प्रमाण: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k चलें","enter_change":"enter बदलें","f2_close":"f2 बंद","esc_close":"esc बंद","j_k_scroll":"j/k स्क्रोल","f3_source":"f3 स्रोत","f3_preview":"f3 पूर्वावलोकन","text_input":"पाठ इनपुट","esc_normal":"esc सामान्य","ctrl_s_save":"ctrl+s सहेजें","ctrl_t_theme":"ctrl+t थीम","tab_focus":"tab फोकस","tab_indent":"tab इंडेंट","ctrl_b_files":"ctrl+b फाइलें","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h इतिहास","enter_open":"enter खोलें","backspace_up":"backspace ऊपर","ctrl_b_close":"ctrl+b बंद","enter_jump":"enter जाएं","r_refresh":"r ताज़ा","ctrl_g_close":"ctrl+g बंद","ctrl_h_close":"ctrl+h बंद","i_insert":"i सम्मिलित","o_open_line":"o पंक्ति खोलें","cc_line":"cc पंक्ति","cw_word":"cw शब्द","ce_word_end":"ce शब्द अंत","c0_start":"c0 शुरुआत","c_end":"c$ अंत","dd_line":"dd पंक्ति","dw_word":"dw शब्द","de_word_end":"de शब्द अंत","d0_start":"d0 शुरुआत","d_end":"d$ अंत","yy_line":"yy पंक्ति","yw_word":"yw शब्द","ye_word_end":"ye शब्द अंत","y0_start":"y0 शुरुआत","y_end":"y$ अंत","gg_top":"gg ऊपर","esc_cancel":"esc रद्द","type_filter":"फ़िल्टर लिखें","arrows_select":"↑/↓ चुनें","slash_filter":"/ फ़िल्टर"},"evidence":{"head":"शीर्ष","tick":"टिक","anchor":"एंकर","coverage":"कवरेज","complete":"पूर्ण","partial":"आंशिक","unavailable":"अनुपलब्ध"}},"history":{"title":"Echo इतिहास","empty":"अभी कोई Echo प्रमाण नहीं","header":"# tick प्रकार स्थिति प्रमाण सारांश"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"खोज","placeholder":"फ़ाइलें छाँटें…","empty":"कोई मेल नहीं","preview_label":"पूर्वावलोकन","preview_loading":"पूर्वावलोकन लोड हो रहा है…","preview_binary":"बाइनरी फ़ाइल — पूर्वावलोकन उपलब्ध नहीं","preview_unavailable":"पूर्वावलोकन उपलब्ध नहीं","preview_truncated":"पूर्वावलोकन संक्षिप्त है"}}; -export const nl: TranslationSchema = {"footer":{"mode":{"browse":"bladeren","insert":"invoegen","normal":"normaal","preview":"voorbeeld","settings":"instellingen","files":"bestanden","graft":"graft","history":"geschiedenis"},"context":{"settings":"instellingen","graft_empty":"open een bestand om te inspecteren","history_empty":"nog geen Echo-bewijs","history_count":"Echo-bewijs: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k bewegen","enter_change":"enter wijzigen","f2_close":"f2 sluiten","esc_close":"esc sluiten","j_k_scroll":"j/k scrollen","f3_source":"f3 bron","f3_preview":"f3 voorbeeld","text_input":"tekstinvoer","esc_normal":"esc normaal","ctrl_s_save":"ctrl+s opslaan","ctrl_t_theme":"ctrl+t thema","tab_focus":"tab focus","tab_indent":"tab inspringen","ctrl_b_files":"ctrl+b bestanden","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h geschiedenis","enter_open":"enter openen","backspace_up":"backspace omhoog","ctrl_b_close":"ctrl+b sluiten","enter_jump":"enter springen","r_refresh":"r vernieuwen","ctrl_g_close":"ctrl+g sluiten","ctrl_h_close":"ctrl+h sluiten","i_insert":"i invoegen","o_open_line":"o regel openen","cc_line":"cc regel","cw_word":"cw woord","ce_word_end":"ce woord-einde","c0_start":"c0 begin","c_end":"c$ einde","dd_line":"dd regel","dw_word":"dw woord","de_word_end":"de woord-einde","d0_start":"d0 begin","d_end":"d$ einde","yy_line":"yy regel","yw_word":"yw woord","ye_word_end":"ye woord-einde","y0_start":"y0 begin","y_end":"y$ einde","gg_top":"gg boven","esc_cancel":"esc annuleren"},"evidence":{"head":"kop","tick":"tick","anchor":"anker","coverage":"dekking","complete":"volledig","partial":"gedeeltelijk","unavailable":"niet beschikbaar"}},"history":{"title":"Echo-geschiedenis","empty":"Nog geen Echo-bewijs","header":"# tick soort status bewijs samenvatting"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const nl: TranslationSchema = {"footer":{"mode":{"browse":"bladeren","insert":"invoegen","normal":"normaal","preview":"voorbeeld","settings":"instellingen","files":"bestanden","graft":"graft","history":"geschiedenis"},"context":{"settings":"instellingen","graft_empty":"open een bestand om te inspecteren","history_empty":"nog geen Echo-bewijs","history_count":"Echo-bewijs: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k bewegen","enter_change":"enter wijzigen","f2_close":"f2 sluiten","esc_close":"esc sluiten","j_k_scroll":"j/k scrollen","f3_source":"f3 bron","f3_preview":"f3 voorbeeld","text_input":"tekstinvoer","esc_normal":"esc normaal","ctrl_s_save":"ctrl+s opslaan","ctrl_t_theme":"ctrl+t thema","tab_focus":"tab focus","tab_indent":"tab inspringen","ctrl_b_files":"ctrl+b bestanden","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h geschiedenis","enter_open":"enter openen","backspace_up":"backspace omhoog","ctrl_b_close":"ctrl+b sluiten","enter_jump":"enter springen","r_refresh":"r vernieuwen","ctrl_g_close":"ctrl+g sluiten","ctrl_h_close":"ctrl+h sluiten","i_insert":"i invoegen","o_open_line":"o regel openen","cc_line":"cc regel","cw_word":"cw woord","ce_word_end":"ce woord-einde","c0_start":"c0 begin","c_end":"c$ einde","dd_line":"dd regel","dw_word":"dw woord","de_word_end":"de woord-einde","d0_start":"d0 begin","d_end":"d$ einde","yy_line":"yy regel","yw_word":"yw woord","ye_word_end":"ye woord-einde","y0_start":"y0 begin","y_end":"y$ einde","gg_top":"gg boven","esc_cancel":"esc annuleren","type_filter":"filter typen","arrows_select":"↑/↓ selecteren","slash_filter":"/ filter"},"evidence":{"head":"kop","tick":"tick","anchor":"anker","coverage":"dekking","complete":"volledig","partial":"gedeeltelijk","unavailable":"niet beschikbaar"}},"history":{"title":"Echo-geschiedenis","empty":"Nog geen Echo-bewijs","header":"# tick soort status bewijs samenvatting"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Zoeken","placeholder":"Bestanden filteren…","empty":"Geen resultaten","preview_label":"Voorbeeld","preview_loading":"Voorbeeld laden…","preview_binary":"Binair bestand — geen voorbeeld","preview_unavailable":"Voorbeeld niet beschikbaar","preview_truncated":"Voorbeeld ingekort"}}; -export const pl: TranslationSchema = {"footer":{"mode":{"browse":"przeglądaj","insert":"wstaw","normal":"normalny","preview":"podgląd","settings":"ustawienia","files":"pliki","graft":"graft","history":"historia"},"context":{"settings":"ustawienia","graft_empty":"otwórz plik do sprawdzenia","history_empty":"brak jeszcze dowodów Echo","history_count":"Dowody Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k przesuń","enter_change":"enter zmień","f2_close":"f2 zamknij","esc_close":"esc zamknij","j_k_scroll":"j/k przewiń","f3_source":"f3 źródło","f3_preview":"f3 podgląd","text_input":"wpisywanie tekstu","esc_normal":"esc normalny","ctrl_s_save":"ctrl+s zapisz","ctrl_t_theme":"ctrl+t motyw","tab_focus":"tab fokus","tab_indent":"tab wcięcie","ctrl_b_files":"ctrl+b pliki","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historia","enter_open":"enter otwórz","backspace_up":"backspace w górę","ctrl_b_close":"ctrl+b zamknij","enter_jump":"enter skocz","r_refresh":"r odśwież","ctrl_g_close":"ctrl+g zamknij","ctrl_h_close":"ctrl+h zamknij","i_insert":"i wstaw","o_open_line":"o otwórz linię","cc_line":"cc linia","cw_word":"cw słowo","ce_word_end":"ce koniec słowa","c0_start":"c0 początek","c_end":"c$ koniec","dd_line":"dd linia","dw_word":"dw słowo","de_word_end":"de koniec słowa","d0_start":"d0 początek","d_end":"d$ koniec","yy_line":"yy linia","yw_word":"yw słowo","ye_word_end":"ye koniec słowa","y0_start":"y0 początek","y_end":"y$ koniec","gg_top":"gg góra","esc_cancel":"esc anuluj"},"evidence":{"head":"głowa","tick":"takt","anchor":"kotwica","coverage":"pokrycie","complete":"pełne","partial":"częściowe","unavailable":"niedostępne"}},"history":{"title":"Historia Echo","empty":"Brak jeszcze dowodów Echo","header":"# tick typ status dowód podsumowanie"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const pl: TranslationSchema = {"footer":{"mode":{"browse":"przeglądaj","insert":"wstaw","normal":"normalny","preview":"podgląd","settings":"ustawienia","files":"pliki","graft":"graft","history":"historia"},"context":{"settings":"ustawienia","graft_empty":"otwórz plik do sprawdzenia","history_empty":"brak jeszcze dowodów Echo","history_count":"Dowody Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k przesuń","enter_change":"enter zmień","f2_close":"f2 zamknij","esc_close":"esc zamknij","j_k_scroll":"j/k przewiń","f3_source":"f3 źródło","f3_preview":"f3 podgląd","text_input":"wpisywanie tekstu","esc_normal":"esc normalny","ctrl_s_save":"ctrl+s zapisz","ctrl_t_theme":"ctrl+t motyw","tab_focus":"tab fokus","tab_indent":"tab wcięcie","ctrl_b_files":"ctrl+b pliki","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h historia","enter_open":"enter otwórz","backspace_up":"backspace w górę","ctrl_b_close":"ctrl+b zamknij","enter_jump":"enter skocz","r_refresh":"r odśwież","ctrl_g_close":"ctrl+g zamknij","ctrl_h_close":"ctrl+h zamknij","i_insert":"i wstaw","o_open_line":"o otwórz linię","cc_line":"cc linia","cw_word":"cw słowo","ce_word_end":"ce koniec słowa","c0_start":"c0 początek","c_end":"c$ koniec","dd_line":"dd linia","dw_word":"dw słowo","de_word_end":"de koniec słowa","d0_start":"d0 początek","d_end":"d$ koniec","yy_line":"yy linia","yw_word":"yw słowo","ye_word_end":"ye koniec słowa","y0_start":"y0 początek","y_end":"y$ koniec","gg_top":"gg góra","esc_cancel":"esc anuluj","type_filter":"wpisz filtr","arrows_select":"↑/↓ wybierz","slash_filter":"/ filtr"},"evidence":{"head":"głowa","tick":"takt","anchor":"kotwica","coverage":"pokrycie","complete":"pełne","partial":"częściowe","unavailable":"niedostępne"}},"history":{"title":"Historia Echo","empty":"Brak jeszcze dowodów Echo","header":"# tick typ status dowód podsumowanie"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Szukaj","placeholder":"Filtruj pliki…","empty":"Brak wyników","preview_label":"Podgląd","preview_loading":"Wczytywanie podglądu…","preview_binary":"Plik binarny — brak podglądu","preview_unavailable":"Podgląd niedostępny","preview_truncated":"Podgląd skrócony"}}; -export const vi: TranslationSchema = {"footer":{"mode":{"browse":"duyệt","insert":"chèn","normal":"thường","preview":"xem trước","settings":"cài đặt","files":"tệp","graft":"graft","history":"lịch sử"},"context":{"settings":"cài đặt","graft_empty":"mở tệp để kiểm tra","history_empty":"chưa có bằng chứng Echo","history_count":"Bằng chứng Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k di chuyển","enter_change":"enter đổi","f2_close":"f2 đóng","esc_close":"esc đóng","j_k_scroll":"j/k cuộn","f3_source":"f3 nguồn","f3_preview":"f3 xem trước","text_input":"nhập văn bản","esc_normal":"esc thường","ctrl_s_save":"ctrl+s lưu","ctrl_t_theme":"ctrl+t giao diện","tab_focus":"tab tiêu điểm","tab_indent":"tab thụt lề","ctrl_b_files":"ctrl+b tệp","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h lịch sử","enter_open":"enter mở","backspace_up":"backspace lên","ctrl_b_close":"ctrl+b đóng","enter_jump":"enter nhảy","r_refresh":"r làm mới","ctrl_g_close":"ctrl+g đóng","ctrl_h_close":"ctrl+h đóng","i_insert":"i chèn","o_open_line":"o mở dòng","cc_line":"cc dòng","cw_word":"cw từ","ce_word_end":"ce cuối từ","c0_start":"c0 đầu","c_end":"c$ cuối","dd_line":"dd dòng","dw_word":"dw từ","de_word_end":"de cuối từ","d0_start":"d0 đầu","d_end":"d$ cuối","yy_line":"yy dòng","yw_word":"yw từ","ye_word_end":"ye cuối từ","y0_start":"y0 đầu","y_end":"y$ cuối","gg_top":"gg đầu","esc_cancel":"esc hủy"},"evidence":{"head":"đầu","tick":"nhịp","anchor":"neo","coverage":"độ phủ","complete":"đầy đủ","partial":"một phần","unavailable":"không khả dụng"}},"history":{"title":"Lịch sử Echo","empty":"Chưa có bằng chứng Echo","header":"# tick loại trạng thái bằng chứng tóm tắt"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const vi: TranslationSchema = {"footer":{"mode":{"browse":"duyệt","insert":"chèn","normal":"thường","preview":"xem trước","settings":"cài đặt","files":"tệp","graft":"graft","history":"lịch sử"},"context":{"settings":"cài đặt","graft_empty":"mở tệp để kiểm tra","history_empty":"chưa có bằng chứng Echo","history_count":"Bằng chứng Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k di chuyển","enter_change":"enter đổi","f2_close":"f2 đóng","esc_close":"esc đóng","j_k_scroll":"j/k cuộn","f3_source":"f3 nguồn","f3_preview":"f3 xem trước","text_input":"nhập văn bản","esc_normal":"esc thường","ctrl_s_save":"ctrl+s lưu","ctrl_t_theme":"ctrl+t giao diện","tab_focus":"tab tiêu điểm","tab_indent":"tab thụt lề","ctrl_b_files":"ctrl+b tệp","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h lịch sử","enter_open":"enter mở","backspace_up":"backspace lên","ctrl_b_close":"ctrl+b đóng","enter_jump":"enter nhảy","r_refresh":"r làm mới","ctrl_g_close":"ctrl+g đóng","ctrl_h_close":"ctrl+h đóng","i_insert":"i chèn","o_open_line":"o mở dòng","cc_line":"cc dòng","cw_word":"cw từ","ce_word_end":"ce cuối từ","c0_start":"c0 đầu","c_end":"c$ cuối","dd_line":"dd dòng","dw_word":"dw từ","de_word_end":"de cuối từ","d0_start":"d0 đầu","d_end":"d$ cuối","yy_line":"yy dòng","yw_word":"yw từ","ye_word_end":"ye cuối từ","y0_start":"y0 đầu","y_end":"y$ cuối","gg_top":"gg đầu","esc_cancel":"esc hủy","type_filter":"nhập bộ lọc","arrows_select":"↑/↓ chọn","slash_filter":"/ lọc"},"evidence":{"head":"đầu","tick":"nhịp","anchor":"neo","coverage":"độ phủ","complete":"đầy đủ","partial":"một phần","unavailable":"không khả dụng"}},"history":{"title":"Lịch sử Echo","empty":"Chưa có bằng chứng Echo","header":"# tick loại trạng thái bằng chứng tóm tắt"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"Tìm","placeholder":"Lọc tệp…","empty":"Không khớp","preview_label":"Xem trước","preview_loading":"Đang tải bản xem trước…","preview_binary":"Tệp nhị phân — không thể xem trước","preview_unavailable":"Không thể xem trước","preview_truncated":"Bản xem trước bị cắt"}}; -export const th: TranslationSchema = {"footer":{"mode":{"browse":"เรียกดู","insert":"แทรก","normal":"ปกติ","preview":"ดูตัวอย่าง","settings":"ตั้งค่า","files":"ไฟล์","graft":"Graft","history":"ประวัติ"},"context":{"settings":"ตั้งค่า","graft_empty":"เปิดไฟล์เพื่อตรวจสอบ","history_empty":"ยังไม่มีหลักฐาน Echo","history_count":"หลักฐาน Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k ย้าย","enter_change":"enter เปลี่ยน","f2_close":"f2 ปิด","esc_close":"esc ปิด","j_k_scroll":"j/k เลื่อน","f3_source":"f3 ต้นฉบับ","f3_preview":"f3 ดูตัวอย่าง","text_input":"ป้อนข้อความ","esc_normal":"esc ปกติ","ctrl_s_save":"ctrl+s บันทึก","ctrl_t_theme":"ctrl+t ธีม","tab_focus":"tab โฟกัส","tab_indent":"tab เยื้อง","ctrl_b_files":"ctrl+b ไฟล์","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h ประวัติ","enter_open":"enter เปิด","backspace_up":"backspace ขึ้น","ctrl_b_close":"ctrl+b ปิด","enter_jump":"enter ไป","r_refresh":"r รีเฟรช","ctrl_g_close":"ctrl+g ปิด","ctrl_h_close":"ctrl+h ปิด","i_insert":"i แทรก","o_open_line":"o เปิดบรรทัด","cc_line":"cc บรรทัด","cw_word":"cw คำ","ce_word_end":"ce จบคำ","c0_start":"c0 เริ่ม","c_end":"c$ จบ","dd_line":"dd บรรทัด","dw_word":"dw คำ","de_word_end":"de จบคำ","d0_start":"d0 เริ่ม","d_end":"d$ จบ","yy_line":"yy บรรทัด","yw_word":"yw คำ","ye_word_end":"ye จบคำ","y0_start":"y0 เริ่ม","y_end":"y$ จบ","gg_top":"gg บน","esc_cancel":"esc ยกเลิก"},"evidence":{"head":"เฮด","tick":"ทิก","anchor":"จุดยึด","coverage":"ความครอบคลุม","complete":"ครบถ้วน","partial":"บางส่วน","unavailable":"ไม่พร้อมใช้งาน"}},"history":{"title":"ประวัติ Echo","empty":"ยังไม่มีหลักฐาน Echo","header":"# tick ชนิด สถานะ หลักฐาน สรุป"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const th: TranslationSchema = {"footer":{"mode":{"browse":"เรียกดู","insert":"แทรก","normal":"ปกติ","preview":"ดูตัวอย่าง","settings":"ตั้งค่า","files":"ไฟล์","graft":"Graft","history":"ประวัติ"},"context":{"settings":"ตั้งค่า","graft_empty":"เปิดไฟล์เพื่อตรวจสอบ","history_empty":"ยังไม่มีหลักฐาน Echo","history_count":"หลักฐาน Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k ย้าย","enter_change":"enter เปลี่ยน","f2_close":"f2 ปิด","esc_close":"esc ปิด","j_k_scroll":"j/k เลื่อน","f3_source":"f3 ต้นฉบับ","f3_preview":"f3 ดูตัวอย่าง","text_input":"ป้อนข้อความ","esc_normal":"esc ปกติ","ctrl_s_save":"ctrl+s บันทึก","ctrl_t_theme":"ctrl+t ธีม","tab_focus":"tab โฟกัส","tab_indent":"tab เยื้อง","ctrl_b_files":"ctrl+b ไฟล์","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h ประวัติ","enter_open":"enter เปิด","backspace_up":"backspace ขึ้น","ctrl_b_close":"ctrl+b ปิด","enter_jump":"enter ไป","r_refresh":"r รีเฟรช","ctrl_g_close":"ctrl+g ปิด","ctrl_h_close":"ctrl+h ปิด","i_insert":"i แทรก","o_open_line":"o เปิดบรรทัด","cc_line":"cc บรรทัด","cw_word":"cw คำ","ce_word_end":"ce จบคำ","c0_start":"c0 เริ่ม","c_end":"c$ จบ","dd_line":"dd บรรทัด","dw_word":"dw คำ","de_word_end":"de จบคำ","d0_start":"d0 เริ่ม","d_end":"d$ จบ","yy_line":"yy บรรทัด","yw_word":"yw คำ","ye_word_end":"ye จบคำ","y0_start":"y0 เริ่ม","y_end":"y$ จบ","gg_top":"gg บน","esc_cancel":"esc ยกเลิก","type_filter":"พิมพ์ตัวกรอง","arrows_select":"↑/↓ เลือก","slash_filter":"/ กรอง"},"evidence":{"head":"เฮด","tick":"ทิก","anchor":"จุดยึด","coverage":"ความครอบคลุม","complete":"ครบถ้วน","partial":"บางส่วน","unavailable":"ไม่พร้อมใช้งาน"}},"history":{"title":"ประวัติ Echo","empty":"ยังไม่มีหลักฐาน Echo","header":"# tick ชนิด สถานะ หลักฐาน สรุป"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"ค้นหา","placeholder":"กรองไฟล์…","empty":"ไม่พบผลลัพธ์","preview_label":"ตัวอย่าง","preview_loading":"กำลังโหลดตัวอย่าง…","preview_binary":"ไฟล์ไบนารี — ไม่สามารถแสดงตัวอย่าง","preview_unavailable":"ไม่สามารถแสดงตัวอย่าง","preview_truncated":"ตัวอย่างถูกตัด"}}; -export const fa: TranslationSchema = {"footer":{"mode":{"browse":"مرور","insert":"درج","normal":"عادی","preview":"پیش‌نمایش","settings":"تنظیمات","files":"فایل‌ها","graft":"Graft","history":"تاریخچه"},"context":{"settings":"تنظیمات","graft_empty":"برای بررسی یک فایل باز کنید","history_empty":"هنوز مدرک Echo وجود ندارد","history_count":"مدارک Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k حرکت","enter_change":"enter تغییر","f2_close":"f2 بستن","esc_close":"esc بستن","j_k_scroll":"j/k پیمایش","f3_source":"f3 منبع","f3_preview":"f3 پیش‌نمایش","text_input":"ورودی متن","esc_normal":"esc عادی","ctrl_s_save":"ctrl+s ذخیره","ctrl_t_theme":"ctrl+t پوسته","tab_focus":"tab تمرکز","tab_indent":"tab تورفتگی","ctrl_b_files":"ctrl+b فایل‌ها","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h تاریخچه","enter_open":"enter باز کردن","backspace_up":"backspace بالا","ctrl_b_close":"ctrl+b بستن","enter_jump":"enter پرش","r_refresh":"r تازه‌سازی","ctrl_g_close":"ctrl+g بستن","ctrl_h_close":"ctrl+h بستن","i_insert":"i درج","o_open_line":"o باز کردن خط","cc_line":"cc خط","cw_word":"cw واژه","ce_word_end":"ce پایان واژه","c0_start":"c0 آغاز","c_end":"c$ پایان","dd_line":"dd خط","dw_word":"dw واژه","de_word_end":"de پایان واژه","d0_start":"d0 آغاز","d_end":"d$ پایان","yy_line":"yy خط","yw_word":"yw واژه","ye_word_end":"ye پایان واژه","y0_start":"y0 آغاز","y_end":"y$ پایان","gg_top":"gg بالا","esc_cancel":"esc لغو"},"evidence":{"head":"سر","tick":"تیک","anchor":"لنگر","coverage":"پوشش","complete":"کامل","partial":"جزئی","unavailable":"در دسترس نیست"}},"history":{"title":"تاریخچه Echo","empty":"هنوز مدرک Echo وجود ندارد","header":"# tick نوع وضعیت مدرک خلاصه"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const fa: TranslationSchema = {"footer":{"mode":{"browse":"مرور","insert":"درج","normal":"عادی","preview":"پیش‌نمایش","settings":"تنظیمات","files":"فایل‌ها","graft":"Graft","history":"تاریخچه"},"context":{"settings":"تنظیمات","graft_empty":"برای بررسی یک فایل باز کنید","history_empty":"هنوز مدرک Echo وجود ندارد","history_count":"مدارک Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k حرکت","enter_change":"enter تغییر","f2_close":"f2 بستن","esc_close":"esc بستن","j_k_scroll":"j/k پیمایش","f3_source":"f3 منبع","f3_preview":"f3 پیش‌نمایش","text_input":"ورودی متن","esc_normal":"esc عادی","ctrl_s_save":"ctrl+s ذخیره","ctrl_t_theme":"ctrl+t پوسته","tab_focus":"tab تمرکز","tab_indent":"tab تورفتگی","ctrl_b_files":"ctrl+b فایل‌ها","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h تاریخچه","enter_open":"enter باز کردن","backspace_up":"backspace بالا","ctrl_b_close":"ctrl+b بستن","enter_jump":"enter پرش","r_refresh":"r تازه‌سازی","ctrl_g_close":"ctrl+g بستن","ctrl_h_close":"ctrl+h بستن","i_insert":"i درج","o_open_line":"o باز کردن خط","cc_line":"cc خط","cw_word":"cw واژه","ce_word_end":"ce پایان واژه","c0_start":"c0 آغاز","c_end":"c$ پایان","dd_line":"dd خط","dw_word":"dw واژه","de_word_end":"de پایان واژه","d0_start":"d0 آغاز","d_end":"d$ پایان","yy_line":"yy خط","yw_word":"yw واژه","ye_word_end":"ye پایان واژه","y0_start":"y0 آغاز","y_end":"y$ پایان","gg_top":"gg بالا","esc_cancel":"esc لغو","type_filter":"نوشتن فیلتر","arrows_select":"↑/↓ انتخاب","slash_filter":"/ فیلتر"},"evidence":{"head":"سر","tick":"تیک","anchor":"لنگر","coverage":"پوشش","complete":"کامل","partial":"جزئی","unavailable":"در دسترس نیست"}},"history":{"title":"تاریخچه Echo","empty":"هنوز مدرک Echo وجود ندارد","header":"# tick نوع وضعیت مدرک خلاصه"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"جستجو","placeholder":"فیلتر پرونده‌ها…","empty":"بدون نتیجه","preview_label":"پیش‌نمایش","preview_loading":"در حال بارگذاری پیش‌نمایش…","preview_binary":"فایل دودویی — پیش‌نمایش در دسترس نیست","preview_unavailable":"پیش‌نمایش در دسترس نیست","preview_truncated":"پیش‌نمایش کوتاه شده"}}; -export const he: TranslationSchema = {"footer":{"mode":{"browse":"עיון","insert":"הוספה","normal":"רגיל","preview":"תצוגה","settings":"הגדרות","files":"קבצים","graft":"Graft","history":"היסטוריה"},"context":{"settings":"הגדרות","graft_empty":"פתח קובץ לבדיקה","history_empty":"עדיין אין ראיות Echo","history_count":"ראיות Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k הזז","enter_change":"enter שנה","f2_close":"f2 סגור","esc_close":"esc סגור","j_k_scroll":"j/k גלול","f3_source":"f3 מקור","f3_preview":"f3 תצוגה","text_input":"קלט טקסט","esc_normal":"esc רגיל","ctrl_s_save":"ctrl+s שמור","ctrl_t_theme":"ctrl+t ערכת נושא","tab_focus":"tab מיקוד","tab_indent":"tab הזחה","ctrl_b_files":"ctrl+b קבצים","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h היסטוריה","enter_open":"enter פתח","backspace_up":"backspace למעלה","ctrl_b_close":"ctrl+b סגור","enter_jump":"enter קפוץ","r_refresh":"r רענן","ctrl_g_close":"ctrl+g סגור","ctrl_h_close":"ctrl+h סגור","i_insert":"i הוסף","o_open_line":"o פתח שורה","cc_line":"cc שורה","cw_word":"cw מילה","ce_word_end":"ce סוף מילה","c0_start":"c0 התחלה","c_end":"c$ סוף","dd_line":"dd שורה","dw_word":"dw מילה","de_word_end":"de סוף מילה","d0_start":"d0 התחלה","d_end":"d$ סוף","yy_line":"yy שורה","yw_word":"yw מילה","ye_word_end":"ye סוף מילה","y0_start":"y0 התחלה","y_end":"y$ סוף","gg_top":"gg למעלה","esc_cancel":"esc בטל"},"evidence":{"head":"ראש","tick":"טיק","anchor":"עוגן","coverage":"כיסוי","complete":"מלא","partial":"חלקי","unavailable":"לא זמין"}},"history":{"title":"היסטוריית Echo","empty":"עדיין אין ראיות Echo","header":"# tick סוג מצב ראיה סיכום"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"}}; +export const he: TranslationSchema = {"footer":{"mode":{"browse":"עיון","insert":"הוספה","normal":"רגיל","preview":"תצוגה","settings":"הגדרות","files":"קבצים","graft":"Graft","history":"היסטוריה"},"context":{"settings":"הגדרות","graft_empty":"פתח קובץ לבדיקה","history_empty":"עדיין אין ראיות Echo","history_count":"ראיות Echo: {count}"},"command":{"invalid":"Command not recognized; type :help for help","hints":{"tab_accept":"tab accept","enter_run":"enter run","esc_cancel":"esc cancel"},"details":{"edit":"Open a file","write":"Write the current file","quit":"Quit jedit","wq":"Write and quit","ttd":"Observe a causal tick without moving canonical head","strand":"Create, switch, or list copy-on-write strands","braid":"View, preview, or admit braid candidates","why":"Explain the last meaningful command","help":"Show command help"}},"hints":{"j_k_move":"j/k הזז","enter_change":"enter שנה","f2_close":"f2 סגור","esc_close":"esc סגור","j_k_scroll":"j/k גלול","f3_source":"f3 מקור","f3_preview":"f3 תצוגה","text_input":"קלט טקסט","esc_normal":"esc רגיל","ctrl_s_save":"ctrl+s שמור","ctrl_t_theme":"ctrl+t ערכת נושא","tab_focus":"tab מיקוד","tab_indent":"tab הזחה","ctrl_b_files":"ctrl+b קבצים","ctrl_g_graft":"ctrl+g graft","ctrl_h_history":"ctrl+h היסטוריה","enter_open":"enter פתח","backspace_up":"backspace למעלה","ctrl_b_close":"ctrl+b סגור","enter_jump":"enter קפוץ","r_refresh":"r רענן","ctrl_g_close":"ctrl+g סגור","ctrl_h_close":"ctrl+h סגור","i_insert":"i הוסף","o_open_line":"o פתח שורה","cc_line":"cc שורה","cw_word":"cw מילה","ce_word_end":"ce סוף מילה","c0_start":"c0 התחלה","c_end":"c$ סוף","dd_line":"dd שורה","dw_word":"dw מילה","de_word_end":"de סוף מילה","d0_start":"d0 התחלה","d_end":"d$ סוף","yy_line":"yy שורה","yw_word":"yw מילה","ye_word_end":"ye סוף מילה","y0_start":"y0 התחלה","y_end":"y$ סוף","gg_top":"gg למעלה","esc_cancel":"esc בטל","type_filter":"הקלדת סינון","arrows_select":"↑/↓ בחירה","slash_filter":"/ סינון"},"evidence":{"head":"ראש","tick":"טיק","anchor":"עוגן","coverage":"כיסוי","complete":"מלא","partial":"חלקי","unavailable":"לא זמין"}},"history":{"title":"היסטוריית Echo","empty":"עדיין אין ראיות Echo","header":"# tick סוג מצב ראיה סיכום"},"startupFileModal":{"title":"Open file","current_directory":"Current directory","empty":"No files in this directory"},"settings":{"sections":{"appearance":"Appearance","editor":"Editor","runtime":"Runtime"},"rows":{"theme":{"label":"Theme","description":"Switch between installed data-driven themes."},"theme_mode":{"label":"Light/dark","description":"Switch the current theme to its light or dark companion."},"footer":{"label":"Footer","description":"Show mode, focus, and command hints at the bottom edge."},"markdown_preview":{"label":"Markdown preview","description":"Switch the active Markdown buffer between source and preview."},"diagnostics":{"label":"Diagnostics","description":"Inspect Graft, parser, and Colorful runtime wiring."},"line_numbers":{"label":"Line numbers","description":"Switch between absolute and cursor-relative editor line numbers."},"gutter_dimmed":{"label":"Dim gutter","description":"Use the theme dimmed gutter token set."},"causal_gutter_basis":{"label":"Causal markers","description":"Choose the causal basis used by gutter change markers."}},"values":{"on":"On","off":"Off","theme_mode_dark":"Dark","theme_mode_light":"Light","line_numbers_absolute":"Absolute","line_numbers_relative":"Relative","source":"Source","preview":"Preview","current":"Current","open":"Open","causal_gutter_last_save":"Last save","causal_gutter_import":"Import","causal_gutter_selected_checkpoint":"Selected checkpoint","causal_gutter_selected_tick":"Selected tick"},"toast":{"changed_title":"Settings changed"}},"worldline":{"title":"Worldlines","empty":"No worldlines yet","header":"kind name basis head delta conflict"},"why":{"range_obstructed_title":"Why range obstructed"},"fileExplorer":{"search_label":"חיפוש","placeholder":"סינון קבצים…","empty":"אין התאמות","preview_label":"תצוגה מקדימה","preview_loading":"טוען תצוגה מקדימה…","preview_binary":"קובץ בינארי — אין תצוגה מקדימה","preview_unavailable":"תצוגה מקדימה אינה זמינה","preview_truncated":"תצוגה מקדימה מקוצרת"}}; -export const me: TranslationSchema = {"footer":{"mode":{"browse":"esworb","insert":"tresni","normal":"lamron","preview":"weiverp","settings":"sgnittes","files":"selif","graft":"tfarg","history":"yrotsih"},"context":{"settings":"sgnittes","graft_empty":"ti tcepsni ot elif a nepo","history_empty":"tey ecnedive ohcE on","history_count":"{count} :ecnedive ohcE"},"command":{"invalid":"pleh rof pleh: epyt ;dezingocer ton dnammoC","hints":{"tab_accept":"tpecca bat","enter_run":"nur retne","esc_cancel":"lecnac cse"},"details":{"edit":"elif a nepO","write":"elif tnerruc eht etirW","quit":"tidej tiuQ","wq":"tiuq dna etirW","ttd":"daeh lacinonac gnivom tuohtiw kcit lasuac a evresbO","strand":"sdnarts etirw-no-ypoc tsil ro ,hctiws ,etaerC","braid":"setadidnac diarb timda ro ,weiverp ,weiV","why":"dnammoc lufgninaem tsal eht nialpxE","help":"pleh dnammoc wohS"}},"hints":{"j_k_move":"evom k/j","enter_change":"egnahc retne","f2_close":"esolc 2f","esc_close":"esolc cse","j_k_scroll":"llorcs k/j","f3_source":"ecruos 3f","f3_preview":"weiverp 3f","text_input":"tupni txet","esc_normal":"lamron cse","ctrl_s_save":"evas s+lrtc","ctrl_t_theme":"emeht t+lrtc","tab_focus":"sucof bat","tab_indent":"tnedni bat","ctrl_b_files":"selif b+lrtc","ctrl_g_graft":"tfarg g+lrtc","ctrl_h_history":"yrotsih h+lrtc","enter_open":"nepo retne","backspace_up":"pu ecapskcab","ctrl_b_close":"esolc b+lrtc","enter_jump":"pmuj retne","r_refresh":"hserfer r","ctrl_g_close":"esolc g+lrtc","ctrl_h_close":"esolc h+lrtc","i_insert":"tresni i","o_open_line":"enil nepo o","cc_line":"enil cc","cw_word":"drow wc","ce_word_end":"dne-drow ec","c0_start":"trats 0c","c_end":"dne $c","dd_line":"enil dd","dw_word":"drow wd","de_word_end":"dne-drow ed","d0_start":"trats 0d","d_end":"dne $d","yy_line":"enil yy","yw_word":"drow wy","ye_word_end":"dne-drow ey","y0_start":"trats 0y","y_end":"dne $y","gg_top":"pot gg","esc_cancel":"lecnac cse"},"evidence":{"head":"daeh","tick":"kcit","anchor":"rohcna","coverage":"egarevoc","complete":"etelpmoc","partial":"laitrap","unavailable":"elbaliavanu"}},"history":{"title":"yrotsiH ohcE","empty":"tey ecnedive ohcE oN","header":"yrammus ecnedive sutats dnik kcit #"},"startupFileModal":{"title":"elif nepO","current_directory":"yrotcerid tnerruC","empty":"yrotcerid siht ni selif oN"},"settings":{"sections":{"appearance":"ecnaraeppA","editor":"rotidE","runtime":"emitnuR"},"rows":{"theme":{"label":"emehT","description":".semeht nevird-atad dellatsni neewteb hctiwS"},"theme_mode":{"label":"krad/thgiL","description":".noinapmoc krad ro thgil sti ot emeht tnerruc eht hctiwS"},"footer":{"label":"retooF","description":".egde mottob eht ta stnih dnammoc dna ,sucof ,edom wohS"},"markdown_preview":{"label":"weiverp nwodkraM","description":".weiverp dna ecruos neewteb reffub nwodkraM evitca eht hctiwS"},"diagnostics":{"label":"scitsongaiD","description":".gniriw emitnur lufroloC dna ,resrap ,tfarG tcepsnI"},"line_numbers":{"label":"srebmun eniL","description":".srebmun enil rotide evitaler-rosruc dna etulosba neewteb hctiwS"},"gutter_dimmed":{"label":"rettug miD","description":".tes nekot rettug demmid emeht eht esU"},"causal_gutter_basis":{"label":"srekram lasuaC","description":".srekram egnahc rettug yb desu sisab lasuac eht esoohC"}},"values":{"on":"nO","off":"ffO","theme_mode_dark":"kraD","theme_mode_light":"thgiL","line_numbers_absolute":"etulosbA","line_numbers_relative":"evitaleR","source":"ecruoS","preview":"weiverP","current":"tnerruC","open":"nepO","causal_gutter_last_save":"evas tsaL","causal_gutter_import":"tropmI","causal_gutter_selected_checkpoint":"tniopkcehc detceleS","causal_gutter_selected_tick":"kcit detceleS"},"toast":{"changed_title":"degnahc sgnitteS"}},"worldline":{"title":"senildlroW","empty":"tey senildlrow oN","header":"tcilfnoc atled daeh sisab eman dnik"},"why":{"range_obstructed_title":"detcurtsbo egnar yhW"}}; +export const me: TranslationSchema = {"footer":{"mode":{"browse":"esworb","insert":"tresni","normal":"lamron","preview":"weiverp","settings":"sgnittes","files":"selif","graft":"tfarg","history":"yrotsih"},"context":{"settings":"sgnittes","graft_empty":"ti tcepsni ot elif a nepo","history_empty":"tey ecnedive ohcE on","history_count":"{count} :ecnedive ohcE"},"command":{"invalid":"pleh rof pleh: epyt ;dezingocer ton dnammoC","hints":{"tab_accept":"tpecca bat","enter_run":"nur retne","esc_cancel":"lecnac cse"},"details":{"edit":"elif a nepO","write":"elif tnerruc eht etirW","quit":"tidej tiuQ","wq":"tiuq dna etirW","ttd":"daeh lacinonac gnivom tuohtiw kcit lasuac a evresbO","strand":"sdnarts etirw-no-ypoc tsil ro ,hctiws ,etaerC","braid":"setadidnac diarb timda ro ,weiverp ,weiV","why":"dnammoc lufgninaem tsal eht nialpxE","help":"pleh dnammoc wohS"}},"hints":{"j_k_move":"evom k/j","enter_change":"egnahc retne","f2_close":"esolc 2f","esc_close":"esolc cse","j_k_scroll":"llorcs k/j","f3_source":"ecruos 3f","f3_preview":"weiverp 3f","text_input":"tupni txet","esc_normal":"lamron cse","ctrl_s_save":"evas s+lrtc","ctrl_t_theme":"emeht t+lrtc","tab_focus":"sucof bat","tab_indent":"tnedni bat","ctrl_b_files":"selif b+lrtc","ctrl_g_graft":"tfarg g+lrtc","ctrl_h_history":"yrotsih h+lrtc","enter_open":"nepo retne","backspace_up":"pu ecapskcab","ctrl_b_close":"esolc b+lrtc","enter_jump":"pmuj retne","r_refresh":"hserfer r","ctrl_g_close":"esolc g+lrtc","ctrl_h_close":"esolc h+lrtc","i_insert":"tresni i","o_open_line":"enil nepo o","cc_line":"enil cc","cw_word":"drow wc","ce_word_end":"dne-drow ec","c0_start":"trats 0c","c_end":"dne $c","dd_line":"enil dd","dw_word":"drow wd","de_word_end":"dne-drow ed","d0_start":"trats 0d","d_end":"dne $d","yy_line":"enil yy","yw_word":"drow wy","ye_word_end":"dne-drow ey","y0_start":"trats 0y","y_end":"dne $y","gg_top":"pot gg","esc_cancel":"lecnac cse","type_filter":"retlif epyt","arrows_select":"tceles ↓/↑","slash_filter":"retlif /"},"evidence":{"head":"daeh","tick":"kcit","anchor":"rohcna","coverage":"egarevoc","complete":"etelpmoc","partial":"laitrap","unavailable":"elbaliavanu"}},"history":{"title":"yrotsiH ohcE","empty":"tey ecnedive ohcE oN","header":"yrammus ecnedive sutats dnik kcit #"},"startupFileModal":{"title":"elif nepO","current_directory":"yrotcerid tnerruC","empty":"yrotcerid siht ni selif oN"},"settings":{"sections":{"appearance":"ecnaraeppA","editor":"rotidE","runtime":"emitnuR"},"rows":{"theme":{"label":"emehT","description":".semeht nevird-atad dellatsni neewteb hctiwS"},"theme_mode":{"label":"krad/thgiL","description":".noinapmoc krad ro thgil sti ot emeht tnerruc eht hctiwS"},"footer":{"label":"retooF","description":".egde mottob eht ta stnih dnammoc dna ,sucof ,edom wohS"},"markdown_preview":{"label":"weiverp nwodkraM","description":".weiverp dna ecruos neewteb reffub nwodkraM evitca eht hctiwS"},"diagnostics":{"label":"scitsongaiD","description":".gniriw emitnur lufroloC dna ,resrap ,tfarG tcepsnI"},"line_numbers":{"label":"srebmun eniL","description":".srebmun enil rotide evitaler-rosruc dna etulosba neewteb hctiwS"},"gutter_dimmed":{"label":"rettug miD","description":".tes nekot rettug demmid emeht eht esU"},"causal_gutter_basis":{"label":"srekram lasuaC","description":".srekram egnahc rettug yb desu sisab lasuac eht esoohC"}},"values":{"on":"nO","off":"ffO","theme_mode_dark":"kraD","theme_mode_light":"thgiL","line_numbers_absolute":"etulosbA","line_numbers_relative":"evitaleR","source":"ecruoS","preview":"weiverP","current":"tnerruC","open":"nepO","causal_gutter_last_save":"evas tsaL","causal_gutter_import":"tropmI","causal_gutter_selected_checkpoint":"tniopkcehc detceleS","causal_gutter_selected_tick":"kcit detceleS"},"toast":{"changed_title":"degnahc sgnitteS"}},"worldline":{"title":"senildlroW","empty":"tey senildlrow oN","header":"tcilfnoc atled daeh sisab eman dnik"},"why":{"range_obstructed_title":"detcurtsbo egnar yhW"},"fileExplorer":{"search_label":"hcraeS","placeholder":"…selif retliF","empty":"sehctam oN","preview_label":"weiverP","preview_loading":"…weiverp gnidaoL","preview_binary":"elbaliavanu weiverp — elif yraniB","preview_unavailable":"elbaliavanu weiverP","preview_truncated":"detacnurt weiverP"}}; export const locales = ["en", "fr", "it", "de", "es", "ko", "ja", "zh-Hans", "pt-BR", "ru", "ar", "id", "tr", "hi", "nl", "pl", "vi", "th", "fa", "he", "me"] as const; diff --git a/src/main-workspace.ts b/src/main-workspace.ts index 0875cf2b..24732887 100644 --- a/src/main-workspace.ts +++ b/src/main-workspace.ts @@ -1,3 +1,5 @@ +import { getDefaultContext } from '@flyingrobots/bijou'; +import { workspaceTerminalIO } from './adapters/workspace-terminal-io.js'; import { initDefaultContext } from '@flyingrobots/bijou-node'; import { run } from '@flyingrobots/bijou-tui'; import { createWorkspaceApp } from './adapters/workspace-app.js'; @@ -72,7 +74,8 @@ export async function runJeditWorkspace(): Promise { initialWorkingDirectory: DEFAULT_WORKING_DIRECTORY, ...workspaceInstrumentationFromEnv(process.env), }, productionText); - await run(app, { mouse: JEDIT_TERMINAL_MOUSE_OPTIONS.mouse }); + const context = getDefaultContext(); + await run(app, { ...JEDIT_TERMINAL_MOUSE_OPTIONS, ctx: { ...context, io: workspaceTerminalIO(context.io) } }); }); } diff --git a/src/ports/file-preview.ts b/src/ports/file-preview.ts new file mode 100644 index 00000000..bdc15d8d --- /dev/null +++ b/src/ports/file-preview.ts @@ -0,0 +1,12 @@ +export const FilePreviewKinds = Object.freeze({ Loaded: 'loaded', Binary: 'binary', Unavailable: 'unavailable' } as const); +export const FILE_PREVIEW_MAX_BYTES = 65_536; +export const FILE_PREVIEW_MAX_LINES = 200; + +export type FilePreviewResult = + | { readonly kind: typeof FilePreviewKinds.Loaded; readonly lines: readonly string[]; readonly truncated: boolean } + | { readonly kind: typeof FilePreviewKinds.Binary } + | { readonly kind: typeof FilePreviewKinds.Unavailable }; + +export interface FilePreviewSource { + readonly loadFilePreview: (path: string) => FilePreviewResult; +} diff --git a/src/ports/file-system.ts b/src/ports/file-system.ts index f29ebf6d..138577bd 100644 --- a/src/ports/file-system.ts +++ b/src/ports/file-system.ts @@ -15,6 +15,7 @@ export interface FileEntry { readonly kind: FileEntryKind; readonly name: string; readonly path: string; + readonly searchPath?: string; } export interface DirectoryIssue { diff --git a/src/ui/file-search-input.ts b/src/ui/file-search-input.ts new file mode 100644 index 00000000..74605548 --- /dev/null +++ b/src/ui/file-search-input.ts @@ -0,0 +1,87 @@ +import { createSurface, stringToSurface, type Surface } from '@flyingrobots/bijou'; +import { clipToWidth } from '@flyingrobots/bijou-tui'; +import type { FileSearchState } from '../app/workspace/file-search.js'; +import type { JeditTheme, JeditStyleToken } from './jedit-theme.js'; +import { fitLine, visibleLineLength } from './fit-line.js'; + +const INPUT_HEIGHT = 1; +const MIN_INPUT_WIDTH = 1; +const EMPTY_INPUT = ''; +const LABEL_SUFFIX = ': '; +const CHARACTER_STEP = 1; + +export interface FileSearchInputOptions { + readonly state: FileSearchState; + readonly label: string; + readonly placeholder: string; + readonly focused: boolean; + readonly width: number; + readonly theme: JeditTheme; +} + +interface FileSearchInputWindow { + readonly prefix: string; + readonly prefixWidth: number; + readonly text: string; + readonly startIndex: number; + readonly cursorColumn: number; +} + +export function renderFileSearchInput(options: FileSearchInputOptions): Surface { + if (options.width <= 0) return createSurface(0, INPUT_HEIGHT); + const window = inputWindow(options); + const content = options.state.input.length === 0 + ? clipToWidth(options.placeholder, options.width - window.prefixWidth) + : window.text; + const surface = stringToSurface(fitLine(`${window.prefix}${content}`, options.width), options.width, INPUT_HEIGHT); + applyInputToken(surface, fileSearchInputToken(options.theme)); + if (options.focused && window.cursorColumn < options.width) { + const cell = surface.get(window.cursorColumn, 0); + surface.set(window.cursorColumn, 0, { ...cell, ...inputCellStyle(options.theme.cursor.insert), empty: false }); + } + return surface; +} + +function inputWindow(options: FileSearchInputOptions): FileSearchInputWindow { + const prefix = clipToWidth(`${options.label}${LABEL_SUFFIX}`, Math.max(0, options.width - MIN_INPUT_WIDTH)); + const prefixWidth = visibleLineLength(prefix); + const capacity = Math.max(MIN_INPUT_WIDTH, options.width - prefixWidth); + const characters = Array.from(options.state.input); + const cursor = Math.max(0, Math.min(characters.length, options.state.cursorIndex)); + let startIndex = 0; + while (startIndex < cursor && visibleLineLength(characters.slice(startIndex, cursor).join(EMPTY_INPUT)) >= capacity) { + startIndex += CHARACTER_STEP; + } + return { + prefix, prefixWidth, startIndex, + text: clipToWidth(characters.slice(startIndex).join(EMPTY_INPUT), capacity), + cursorColumn: prefixWidth + visibleLineLength(characters.slice(startIndex, cursor).join(EMPTY_INPUT)), + }; +} + +export function fileSearchCursorAtColumn(options: FileSearchInputOptions, column: number): number { + const window = inputWindow(options); + const characters = Array.from(options.state.input); + let position = window.prefixWidth; + for (let index = window.startIndex; index < characters.length; index += CHARACTER_STEP) { + if (column <= position) return index; + position += visibleLineLength(characters[index] ?? EMPTY_INPUT); + } + return characters.length; +} + +function applyInputToken(surface: Surface, token: JeditStyleToken): void { + for (let x = 0; x < surface.width; x += CHARACTER_STEP) { + surface.set(x, 0, { ...surface.get(x, 0), ...inputCellStyle(token), empty: false }); + } +} + +function inputCellStyle(token: JeditStyleToken) { + return { fg: token.fg, fgRGB: token.fgRGB, bg: token.bg, bgRGB: token.bgRGB, modifiers: token.modifiers == null ? undefined : [...token.modifiers] }; +} + +function fileSearchInputToken(theme: JeditTheme): JeditStyleToken { + return theme.surface.currentLine.bg === theme.surface.drawer.bg + ? theme.surface.workspace + : theme.surface.currentLine; +} diff --git a/src/ui/file-search-results.ts b/src/ui/file-search-results.ts new file mode 100644 index 00000000..956c2405 --- /dev/null +++ b/src/ui/file-search-results.ts @@ -0,0 +1,60 @@ +import type { Surface } from '@flyingrobots/bijou'; +import type { FileSearchEntry } from '../app/workspace/file-search.js'; +import { legibleOn } from './theme-contrast.js'; +import type { JeditTheme, JeditStyleToken } from './jedit-theme.js'; +import { JEDIT_TEXT_MODIFIER } from './jedit-theme.js'; +import { visibleLineLength } from './fit-line.js'; + +const ENTRY_PREFIX_WIDTH = 2; +const CHARACTER_STEP = 1; +const MIN_SELECTION_CONTRAST = 4.5; +const HEX_RADIX = 16; +const HEX_PAIR_WIDTH = 2; + +export function highlightFileSearchMatches(surface: Surface, entries: readonly FileSearchEntry[], theme: JeditTheme, selection: FileSearchSelection): void { + entries.forEach(({ entry, matchIndexes, index: sourceIndex }, row) => { + const selected = sourceIndex === selection.selectedIndex; + paintFileSearchRow(surface, row, theme, { selected, hovered: sourceIndex === selection.hoveredIndex }); + const characters = Array.from(entry.name); + for (const index of matchIndexes) { + const column = ENTRY_PREFIX_WIDTH + visibleLineLength(characters.slice(0, index).join('')); + const width = visibleLineLength(characters[index] ?? ''); + for (let x = column; x < Math.min(surface.width, column + width); x += CHARACTER_STEP) { + const cell = surface.get(x, row); + surface.set(x, row, { + ...cell, fg: selected ? cell.fg : theme.chrome.activeEdge.fg, + fgRGB: selected ? cell.fgRGB : theme.chrome.activeEdge.fgRGB, + modifiers: [...(cell.modifiers ?? []), JEDIT_TEXT_MODIFIER.Bold], + }); + } + } + }); +} + +export interface FileSearchSelection { + readonly selectedIndex: number; + readonly hoveredIndex?: number; +} + +interface FileSearchRowState { + readonly selected: boolean; + readonly hovered: boolean; +} + +function paintFileSearchRow(surface: Surface, row: number, theme: JeditTheme, state: FileSearchRowState): void { + if (!state.selected && !state.hovered) return; + const token = state.selected ? fileSelectionToken(theme) : theme.surface.currentLine; + for (let x = 0; x < surface.width; x += CHARACTER_STEP) { + const cell = surface.get(x, row); + surface.set(x, row, { ...cell, fg: token.fg, fgRGB: token.fgRGB, bg: token.bg, bgRGB: token.bgRGB }); + } +} + +function fileSelectionToken(theme: JeditTheme): JeditStyleToken { + const background = theme.cursor.normal.bgRGB; + const foreground = theme.surface.workspace.bgRGB; + if (background == null || foreground == null) return theme.cursor.normal; + const fgRGB = legibleOn(foreground, [background], MIN_SELECTION_CONTRAST); + const fg = `#${fgRGB.map((channel) => channel.toString(HEX_RADIX).padStart(HEX_PAIR_WIDTH, '0')).join('')}`; + return { ...theme.cursor.normal, fg, fgRGB }; +} diff --git a/src/ui/terminal-mouse.ts b/src/ui/terminal-mouse.ts index d73490a9..2eab6a5f 100644 --- a/src/ui/terminal-mouse.ts +++ b/src/ui/terminal-mouse.ts @@ -1,3 +1,4 @@ export const JEDIT_TERMINAL_MOUSE_OPTIONS = { mouse: true, + mouseMode: 'any', } as const; diff --git a/src/ui/workspace-chrome.ts b/src/ui/workspace-chrome.ts index 2c3307a2..24eb4663 100644 --- a/src/ui/workspace-chrome.ts +++ b/src/ui/workspace-chrome.ts @@ -40,6 +40,9 @@ const EditorModeKeys: Record = Object.freeze({ }); const FooterHintKeys = Object.freeze({ + TypeFilter: 'type_filter', + SlashFilter: 'slash_filter', + ArrowsSelect: 'arrows_select', JkMove: 'j_k_move', EnterChange: 'enter_change', F2Close: 'f2_close', @@ -94,6 +97,7 @@ export interface WorkspaceFooterState { readonly i18n: I18nPort; readonly focusPane: FocusPane; readonly fileDrawerOpen: boolean; + readonly fileSearchEditing?: boolean; readonly graftDrawerOpen: boolean; readonly viewMode: ViewMode; readonly markdownPreviewActive: boolean; @@ -322,9 +326,9 @@ function previewFooterHints(state: WorkspaceFooterState, t: FooterHintTranslator function fileDrawerFooterHints(state: WorkspaceFooterState, t: FooterHintTranslator): ReadonlyArray { return [ - t(FooterHintKeys.JkMove), - t(FooterHintKeys.EnterOpen), - t(FooterHintKeys.BackspaceUp), + t(state.fileSearchEditing ? FooterHintKeys.TypeFilter : FooterHintKeys.JkMove), + t(state.fileSearchEditing ? FooterHintKeys.EscNormal : FooterHintKeys.SlashFilter), + state.fileSearchEditing ? undefined : t(FooterHintKeys.EnterOpen), t(FooterHintKeys.CtrlBClose), focusHint(state, t), ]; diff --git a/translations.json b/translations.json index d63a553f..e7060a50 100644 --- a/translations.json +++ b/translations.json @@ -82,7 +82,10 @@ "y0_start": "y0 start", "y_end": "y$ end", "gg_top": "gg top", - "esc_cancel": "esc cancel" + "esc_cancel": "esc cancel", + "type_filter": "type filter", + "arrows_select": "↑/↓ select", + "slash_filter": "/ filter" }, "evidence": { "head": "head", @@ -171,6 +174,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Search", + "placeholder": "Filter files…", + "empty": "No matches", + "preview_label": "Preview", + "preview_loading": "Loading preview…", + "preview_binary": "Binary file — preview unavailable", + "preview_unavailable": "Preview unavailable", + "preview_truncated": "Preview truncated" } } }, @@ -256,7 +269,10 @@ "y0_start": "y0 début", "y_end": "y$ fin", "gg_top": "gg haut", - "esc_cancel": "esc annuler" + "esc_cancel": "esc annuler", + "type_filter": "saisir le filtre", + "arrows_select": "↑/↓ sélectionner", + "slash_filter": "/ filtrer" }, "evidence": { "head": "tête", @@ -345,6 +361,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Recherche", + "placeholder": "Filtrer les fichiers…", + "empty": "Aucun résultat", + "preview_label": "Aperçu", + "preview_loading": "Chargement de l’aperçu…", + "preview_binary": "Fichier binaire — aperçu indisponible", + "preview_unavailable": "Aperçu indisponible", + "preview_truncated": "Aperçu tronqué" } } }, @@ -430,7 +456,10 @@ "y0_start": "y0 inizio", "y_end": "y$ fine", "gg_top": "gg cima", - "esc_cancel": "esc annulla" + "esc_cancel": "esc annulla", + "type_filter": "digita filtro", + "arrows_select": "↑/↓ seleziona", + "slash_filter": "/ filtra" }, "evidence": { "head": "testa", @@ -519,6 +548,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Cerca", + "placeholder": "Filtra file…", + "empty": "Nessun risultato", + "preview_label": "Anteprima", + "preview_loading": "Caricamento anteprima…", + "preview_binary": "File binario — anteprima non disponibile", + "preview_unavailable": "Anteprima non disponibile", + "preview_truncated": "Anteprima troncata" } } }, @@ -604,7 +643,10 @@ "y0_start": "y0 anfang", "y_end": "y$ ende", "gg_top": "gg oben", - "esc_cancel": "esc abbrechen" + "esc_cancel": "esc abbrechen", + "type_filter": "Filter eingeben", + "arrows_select": "↑/↓ auswählen", + "slash_filter": "/ filtern" }, "evidence": { "head": "Kopf", @@ -693,6 +735,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Suche", + "placeholder": "Dateien filtern…", + "empty": "Keine Treffer", + "preview_label": "Vorschau", + "preview_loading": "Vorschau wird geladen…", + "preview_binary": "Binärdatei — keine Vorschau", + "preview_unavailable": "Vorschau nicht verfügbar", + "preview_truncated": "Vorschau gekürzt" } } }, @@ -778,7 +830,10 @@ "y0_start": "y0 inicio", "y_end": "y$ fin", "gg_top": "gg arriba", - "esc_cancel": "esc cancelar" + "esc_cancel": "esc cancelar", + "type_filter": "escribir filtro", + "arrows_select": "↑/↓ seleccionar", + "slash_filter": "/ filtrar" }, "evidence": { "head": "cabeza", @@ -867,6 +922,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Buscar", + "placeholder": "Filtrar archivos…", + "empty": "Sin resultados", + "preview_label": "Vista previa", + "preview_loading": "Cargando vista previa…", + "preview_binary": "Archivo binario — sin vista previa", + "preview_unavailable": "Vista previa no disponible", + "preview_truncated": "Vista previa truncada" } } }, @@ -952,7 +1017,10 @@ "y0_start": "y0 시작", "y_end": "y$ 끝", "gg_top": "gg 맨 위", - "esc_cancel": "esc 취소" + "esc_cancel": "esc 취소", + "type_filter": "필터 입력", + "arrows_select": "↑/↓ 선택", + "slash_filter": "/ 필터" }, "evidence": { "head": "헤드", @@ -1041,6 +1109,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "검색", + "placeholder": "파일 필터…", + "empty": "일치 항목 없음", + "preview_label": "미리 보기", + "preview_loading": "미리 보기 불러오는 중…", + "preview_binary": "바이너리 파일 — 미리 보기 불가", + "preview_unavailable": "미리 보기 불가", + "preview_truncated": "미리 보기 생략됨" } } }, @@ -1126,7 +1204,10 @@ "y0_start": "y0 先頭", "y_end": "y$ 末尾", "gg_top": "gg 先頭", - "esc_cancel": "esc 取消" + "esc_cancel": "esc 取消", + "type_filter": "絞り込み入力", + "arrows_select": "↑/↓ 選択", + "slash_filter": "/ 絞り込み" }, "evidence": { "head": "ヘッド", @@ -1215,6 +1296,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "検索", + "placeholder": "ファイルを絞り込む…", + "empty": "一致なし", + "preview_label": "プレビュー", + "preview_loading": "プレビューを読み込み中…", + "preview_binary": "バイナリファイル — プレビュー不可", + "preview_unavailable": "プレビュー不可", + "preview_truncated": "プレビューを省略" } } }, @@ -1300,7 +1391,10 @@ "y0_start": "y0 开头", "y_end": "y$ 结尾", "gg_top": "gg 顶部", - "esc_cancel": "esc 取消" + "esc_cancel": "esc 取消", + "type_filter": "输入筛选", + "arrows_select": "↑/↓ 选择", + "slash_filter": "/ 筛选" }, "evidence": { "head": "头", @@ -1389,6 +1483,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "搜索", + "placeholder": "筛选文件…", + "empty": "无匹配项", + "preview_label": "预览", + "preview_loading": "正在加载预览…", + "preview_binary": "二进制文件 — 无法预览", + "preview_unavailable": "无法预览", + "preview_truncated": "预览已截断" } } }, @@ -1474,7 +1578,10 @@ "y0_start": "y0 início", "y_end": "y$ fim", "gg_top": "gg topo", - "esc_cancel": "esc cancelar" + "esc_cancel": "esc cancelar", + "type_filter": "digitar filtro", + "arrows_select": "↑/↓ selecionar", + "slash_filter": "/ filtrar" }, "evidence": { "head": "cabeça", @@ -1563,6 +1670,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Buscar", + "placeholder": "Filtrar arquivos…", + "empty": "Nenhum resultado", + "preview_label": "Prévia", + "preview_loading": "Carregando prévia…", + "preview_binary": "Arquivo binário — sem prévia", + "preview_unavailable": "Prévia indisponível", + "preview_truncated": "Prévia truncada" } } }, @@ -1648,7 +1765,10 @@ "y0_start": "y0 начало", "y_end": "y$ конец", "gg_top": "gg верх", - "esc_cancel": "esc отмена" + "esc_cancel": "esc отмена", + "type_filter": "ввод фильтра", + "arrows_select": "↑/↓ выбор", + "slash_filter": "/ фильтр" }, "evidence": { "head": "вершина", @@ -1737,6 +1857,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Поиск", + "placeholder": "Фильтр файлов…", + "empty": "Нет совпадений", + "preview_label": "Просмотр", + "preview_loading": "Загрузка просмотра…", + "preview_binary": "Двоичный файл — просмотр недоступен", + "preview_unavailable": "Просмотр недоступен", + "preview_truncated": "Просмотр сокращён" } } }, @@ -1822,7 +1952,10 @@ "y0_start": "y0 بداية", "y_end": "y$ نهاية", "gg_top": "gg أعلى", - "esc_cancel": "esc إلغاء" + "esc_cancel": "esc إلغاء", + "type_filter": "اكتب للتصفية", + "arrows_select": "↑/↓ تحديد", + "slash_filter": "/ تصفية" }, "evidence": { "head": "الرأس", @@ -1911,6 +2044,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "بحث", + "placeholder": "تصفية الملفات…", + "empty": "لا نتائج", + "preview_label": "معاينة", + "preview_loading": "جارٍ تحميل المعاينة…", + "preview_binary": "ملف ثنائي — المعاينة غير متاحة", + "preview_unavailable": "المعاينة غير متاحة", + "preview_truncated": "المعاينة مختصرة" } } }, @@ -1996,7 +2139,10 @@ "y0_start": "y0 awal", "y_end": "y$ akhir", "gg_top": "gg atas", - "esc_cancel": "esc batal" + "esc_cancel": "esc batal", + "type_filter": "ketik filter", + "arrows_select": "↑/↓ pilih", + "slash_filter": "/ filter" }, "evidence": { "head": "kepala", @@ -2085,6 +2231,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Cari", + "placeholder": "Filter berkas…", + "empty": "Tidak ada hasil", + "preview_label": "Pratinjau", + "preview_loading": "Memuat pratinjau…", + "preview_binary": "Berkas biner — pratinjau tidak tersedia", + "preview_unavailable": "Pratinjau tidak tersedia", + "preview_truncated": "Pratinjau dipotong" } } }, @@ -2170,7 +2326,10 @@ "y0_start": "y0 başlangıç", "y_end": "y$ son", "gg_top": "gg üst", - "esc_cancel": "esc iptal" + "esc_cancel": "esc iptal", + "type_filter": "filtre yaz", + "arrows_select": "↑/↓ seç", + "slash_filter": "/ filtre" }, "evidence": { "head": "baş", @@ -2259,6 +2418,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Ara", + "placeholder": "Dosyaları filtrele…", + "empty": "Eşleşme yok", + "preview_label": "Önizleme", + "preview_loading": "Önizleme yükleniyor…", + "preview_binary": "İkili dosya — önizleme yok", + "preview_unavailable": "Önizleme kullanılamıyor", + "preview_truncated": "Önizleme kısaltıldı" } } }, @@ -2344,7 +2513,10 @@ "y0_start": "y0 शुरुआत", "y_end": "y$ अंत", "gg_top": "gg ऊपर", - "esc_cancel": "esc रद्द" + "esc_cancel": "esc रद्द", + "type_filter": "फ़िल्टर लिखें", + "arrows_select": "↑/↓ चुनें", + "slash_filter": "/ फ़िल्टर" }, "evidence": { "head": "शीर्ष", @@ -2433,6 +2605,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "खोज", + "placeholder": "फ़ाइलें छाँटें…", + "empty": "कोई मेल नहीं", + "preview_label": "पूर्वावलोकन", + "preview_loading": "पूर्वावलोकन लोड हो रहा है…", + "preview_binary": "बाइनरी फ़ाइल — पूर्वावलोकन उपलब्ध नहीं", + "preview_unavailable": "पूर्वावलोकन उपलब्ध नहीं", + "preview_truncated": "पूर्वावलोकन संक्षिप्त है" } } }, @@ -2518,7 +2700,10 @@ "y0_start": "y0 begin", "y_end": "y$ einde", "gg_top": "gg boven", - "esc_cancel": "esc annuleren" + "esc_cancel": "esc annuleren", + "type_filter": "filter typen", + "arrows_select": "↑/↓ selecteren", + "slash_filter": "/ filter" }, "evidence": { "head": "kop", @@ -2607,6 +2792,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Zoeken", + "placeholder": "Bestanden filteren…", + "empty": "Geen resultaten", + "preview_label": "Voorbeeld", + "preview_loading": "Voorbeeld laden…", + "preview_binary": "Binair bestand — geen voorbeeld", + "preview_unavailable": "Voorbeeld niet beschikbaar", + "preview_truncated": "Voorbeeld ingekort" } } }, @@ -2692,7 +2887,10 @@ "y0_start": "y0 początek", "y_end": "y$ koniec", "gg_top": "gg góra", - "esc_cancel": "esc anuluj" + "esc_cancel": "esc anuluj", + "type_filter": "wpisz filtr", + "arrows_select": "↑/↓ wybierz", + "slash_filter": "/ filtr" }, "evidence": { "head": "głowa", @@ -2781,6 +2979,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Szukaj", + "placeholder": "Filtruj pliki…", + "empty": "Brak wyników", + "preview_label": "Podgląd", + "preview_loading": "Wczytywanie podglądu…", + "preview_binary": "Plik binarny — brak podglądu", + "preview_unavailable": "Podgląd niedostępny", + "preview_truncated": "Podgląd skrócony" } } }, @@ -2866,7 +3074,10 @@ "y0_start": "y0 đầu", "y_end": "y$ cuối", "gg_top": "gg đầu", - "esc_cancel": "esc hủy" + "esc_cancel": "esc hủy", + "type_filter": "nhập bộ lọc", + "arrows_select": "↑/↓ chọn", + "slash_filter": "/ lọc" }, "evidence": { "head": "đầu", @@ -2955,6 +3166,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "Tìm", + "placeholder": "Lọc tệp…", + "empty": "Không khớp", + "preview_label": "Xem trước", + "preview_loading": "Đang tải bản xem trước…", + "preview_binary": "Tệp nhị phân — không thể xem trước", + "preview_unavailable": "Không thể xem trước", + "preview_truncated": "Bản xem trước bị cắt" } } }, @@ -3040,7 +3261,10 @@ "y0_start": "y0 เริ่ม", "y_end": "y$ จบ", "gg_top": "gg บน", - "esc_cancel": "esc ยกเลิก" + "esc_cancel": "esc ยกเลิก", + "type_filter": "พิมพ์ตัวกรอง", + "arrows_select": "↑/↓ เลือก", + "slash_filter": "/ กรอง" }, "evidence": { "head": "เฮด", @@ -3129,6 +3353,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "ค้นหา", + "placeholder": "กรองไฟล์…", + "empty": "ไม่พบผลลัพธ์", + "preview_label": "ตัวอย่าง", + "preview_loading": "กำลังโหลดตัวอย่าง…", + "preview_binary": "ไฟล์ไบนารี — ไม่สามารถแสดงตัวอย่าง", + "preview_unavailable": "ไม่สามารถแสดงตัวอย่าง", + "preview_truncated": "ตัวอย่างถูกตัด" } } }, @@ -3214,7 +3448,10 @@ "y0_start": "y0 آغاز", "y_end": "y$ پایان", "gg_top": "gg بالا", - "esc_cancel": "esc لغو" + "esc_cancel": "esc لغو", + "type_filter": "نوشتن فیلتر", + "arrows_select": "↑/↓ انتخاب", + "slash_filter": "/ فیلتر" }, "evidence": { "head": "سر", @@ -3303,6 +3540,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "جستجو", + "placeholder": "فیلتر پرونده‌ها…", + "empty": "بدون نتیجه", + "preview_label": "پیش‌نمایش", + "preview_loading": "در حال بارگذاری پیش‌نمایش…", + "preview_binary": "فایل دودویی — پیش‌نمایش در دسترس نیست", + "preview_unavailable": "پیش‌نمایش در دسترس نیست", + "preview_truncated": "پیش‌نمایش کوتاه شده" } } }, @@ -3388,7 +3635,10 @@ "y0_start": "y0 התחלה", "y_end": "y$ סוף", "gg_top": "gg למעלה", - "esc_cancel": "esc בטל" + "esc_cancel": "esc בטל", + "type_filter": "הקלדת סינון", + "arrows_select": "↑/↓ בחירה", + "slash_filter": "/ סינון" }, "evidence": { "head": "ראש", @@ -3477,6 +3727,16 @@ }, "why": { "range_obstructed_title": "Why range obstructed" + }, + "fileExplorer": { + "search_label": "חיפוש", + "placeholder": "סינון קבצים…", + "empty": "אין התאמות", + "preview_label": "תצוגה מקדימה", + "preview_loading": "טוען תצוגה מקדימה…", + "preview_binary": "קובץ בינארי — אין תצוגה מקדימה", + "preview_unavailable": "תצוגה מקדימה אינה זמינה", + "preview_truncated": "תצוגה מקדימה מקוצרת" } } }