Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions design.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -215,6 +215,8 @@ Owns the `Map<UriString, Document>`, exposes `tecode.workspace.openDocument/docu

Layout state `{ sidebarVisible, sidebarWidth, panelVisible, panelHeight, activeView }` persists to `~/.config/tecode/state.json` on change (debounced) and on exit (*Req 6.4*).

**Initial editor focus** (*Req 6.7*): `EditorArea` gives its `EditorView`'s text plane keyboard focus the moment a document becomes the active tab — covering startup with a document already open, the first document opening on an empty workspace, and switching tabs — so typing works with no manual focus action. It skips this whenever the command palette, an input box, the find widget, or the explorer sidebar currently holds focus (read back through the shared context service, since none of those are `EditorArea`'s own descendants), so it never steals focus from something the user deliberately focused.

### 8.3 EditorView (custom component, decision #2)

Layers, back to front:
Expand Down
43 changes: 36 additions & 7 deletions packages/cli/src/editingHarness.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -49,6 +49,7 @@ import {
applyConfiguredTheme,
ContextFocusTracker,
getUserExtensionsDir,
ModalOverlay,
Shell,
ThemeProvider,
type ContextService,
Expand DownExpand Up@@ -309,6 +310,24 @@ function findAllFocusable(node: unknown): FocusableLike[] {
* Returns `true` once a focused node is found that actually sets the key;
* `false` (leaving every candidate focused-then-blurred) if none does —
* e.g. no document is open yet, so `EditorArea` has no text plane to find.
*
* **Does NOT answer "who grants focus in the first place?"** (Issue #82's
* post-mortem): before the fix in `ui/shell.tsx`'s `EditorArea`, NOTHING in
* production ever imperatively focused the text plane on mount — this
* helper's own real-`.focus()`-walk masked that gap for over a year of
* tests, because every test that types goes through this function first,
* never through the production mount path alone. `EditorArea` now grants
* initial focus itself (on mount with a document already open, on a
* document opening later, and on switching tabs — see that component's own
* TSDoc for the exact rule and its do-not-steal guard), so a test that
* wants to prove typing works FROM PRODUCTION STARTUP ALONE must render the
* Shell and type WITHOUT calling this helper at all
* (`shell.initialFocus.test.tsx`'s "no focus assist" tests are exactly
* that) — calling `focusEditorText` before typing is still correct and
* still the right tool for every test that has a different, unrelated
* thing to prove (multi-cursor, undo/redo, syntax highlighting, …), it
* just no longer stands in for "does startup focus the editor" the way its
* mere existence previously, silently did.
*/
export function focusEditorText(rendererRoot: unknown, context: Pick<ContextService, "get">): boolean {
for (const node of findAllFocusable(rendererRoot)) {
Expand DownExpand Up@@ -339,16 +358,25 @@ export type EditingShellDeps = Pick<
| "editorSession"
| "findService"
| "highlightService"
| "modalService"
>;

/**
* Mount `<ThemeProvider><ContextFocusTracker><Shell/></ContextFocusTracker>
* </ThemeProvider>` (design.md §8.1's component tree) onto OpenTUI's
* headless test renderer — the exact same tree
* `renderShell.tsx`'s `renderShellToTerminal` mounts onto a real terminal,
* just onto `@opentui/react/test-utils`'s `testRender` instead of a real
* `CliRenderer` (`shell.snapshot.test.tsx`'s top-of-file TSDoc documents why this is
* a full, real cell-grid renderer, not a fallback).
* Mount `<ThemeProvider><ContextFocusTracker><Shell/><ModalOverlay/>
* </ContextFocusTracker></ThemeProvider>` (design.md §8.1's component tree,
* `modalOverlay.tsx`'s "Mount point") onto OpenTUI's headless test
* renderer — the exact same tree `renderShell.tsx`'s `renderShellToTerminal`
* mounts onto a real terminal, just onto `@opentui/react/test-utils`'s
* `testRender` instead of a real `CliRenderer` (`shell.snapshot.test.tsx`'s
* top-of-file TSDoc documents why this is a full, real cell-grid renderer,
* not a fallback). `ModalOverlay` is mounted unconditionally (matching
* `renderShellToTerminal`'s own always-there `modalService` in production
* `main.ts`), so a test can drive `root.modalService.openQuickPick(...)`/
* `openInputBox(...)` and observe the SAME `quickPickFocus`/`inputBoxFocus`
* context transitions production reports — Issue #82's "do not steal focus
* from the palette" regression is a real ordering interaction between
* `ModalOverlay` and `Shell`'s `EditorArea` that a harness omitting
* `ModalOverlay` could never exercise.
*/
export function renderEditingShell(
deps: EditingShellDeps,
Expand All@@ -367,6 +395,7 @@ export function renderEditingShell(
findService={deps.findService}
highlightService={deps.highlightService}
/>
<ModalOverlay modalService={deps.modalService} />
</ContextFocusTracker>
</ThemeProvider>
);
Expand Down
38 changes: 38 additions & 0 deletions packages/core/src/ui/focus.tsx
Original file line numberDiff line numberDiff line change
Expand Up@@ -66,6 +66,44 @@ export function ContextFocusTracker(props: ContextFocusTrackerProps): ReactNode
);
}

/**
* Returns the {@link ContextService} the nearest {@link ContextFocusTracker}
* provides, narrowed to `get`/`onDidChange` (Req 4.6) — for a component
* that needs to READ another region's focus-tracked context key directly
* (rather than only report its OWN focus transitions via
* {@link useFocusTracking}). `undefined` outside a
* {@link ContextFocusTracker} — matches this module's "no-op rather than
* throw when unwrapped" discipline for {@link useFocusTracking} itself, and
* a caller reads it the same way: an `undefined` context conservatively
* means "nothing is known to be holding focus" (`ui/shell.tsx`'s
* `EditorArea` initial-focus guard, Issue #82, is the first consumer — it
* must not steal focus from the command palette (`quickPickFocus`), an
* input box (`inputBoxFocus`), the find widget (`findWidgetFocus`), or the
* explorer (`explorerFocus`), none of which are `EditorArea`'s own React
* descendants: `ModalOverlay` is `Shell`'s sibling, `Sidebar` is `Shell`'s
* child — so the only way to see those keys from inside `EditorArea` is
* back through this shared `ContextService`, not through the component
* tree).
*
* **`onDidChange`** (CodeRabbit PR #83 follow-up on Issue #82's fix):
* `ContextService.onDidChange` is otherwise host-internal — "consumed by
* focus tracking and the keymap service, not extensions" (`api/create.ts`'s
* TSDoc on why `tecode.context` never exposes it). Exposing it here, to a
* component that already reads focus-tracked keys through this same hook,
* stays within that boundary (still core-internal, still nothing an
* extension can reach through `tecode.context`) while giving
* `EditorArea`'s do-not-steal guard a way to be told when a guard it
* deferred on has since cleared — a change to `quickPickFocus`/
* `inputBoxFocus`/`findWidgetFocus`/`explorerFocus` is otherwise invisible
* to `EditorArea`'s own re-render cycle, since none of those keys are its
* own props and this hook always returns the SAME `ContextService`
* instance (no new value, hence no dependency-array-triggered re-run, ever
* comes from `focusContext` itself changing).
*/
export function useFocusContextService(): Pick<ContextService, "get" | "onDidChange"> | undefined {
return useContext(FocusContextServiceContext);
}

/**
* Returns a `ref` callback that reports `key`'s value (`true`/`false`) to
* the {@link ContextFocusTracker}-provided context service whenever the
Expand Down
Loading
Loading