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
220 changes: 220 additions & 0 deletions bun.lock

Large diffs are not rendered by default.

8 changes: 7 additions & 1 deletion packages/core/package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -6,6 +6,12 @@
"main": "src/index.ts",
"types": "src/index.ts",
"dependencies": {
"@tecode/api": "workspace:*"
"@tecode/api": "workspace:*",
"@opentui/core": "^0.1.107",
"@opentui/react": "^0.1.107",
"react": "^19.0.0"
},
"devDependencies": {
"@types/react": "^19.0.0"
}
}
42 changes: 34 additions & 8 deletions packages/core/src/api/create.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -44,11 +44,12 @@ import type { DocumentManager } from "../buffer/documentManager";
import type { ConfigService } from "../config/service";
import type { ContextService } from "../keymap/context";
import type { StatusSink } from "../host/errors";
import { Input, List, Tabs, Tree } from "../ui/components";
import { createSlotRegistry, type SlotRegistry } from "../ui/slotRegistry";
import {
createEditorStub,
createLanguagesStub,
createThemesStub,
createUiStub,
createWindowStub,
} from "./stubs";

Expand DownExpand Up@@ -77,6 +78,20 @@ export interface CreateTecodeApiDeps {
* 10.1, design.md §12's "no-active-editor no-ops with a status-bar
* notice"). */
sink: StatusSink;
/**
* Backs `tecode.ui.registerView` (Req 6.3, 10.1; design.md §8.2; Task
* 1.14) — the live slot registry the Shell's regions render from.
* Optional: a caller that has not wired discovery/activation yet (every
* existing test in this suite, and any future caller that only needs the
* namespace's shape) gets a registry built with no pending manifest
* views and no activation hook — `registerView`'s register/dispose
* symmetry still holds fully; only lazy-view activation
* (`ui/slotRegistry.ts`'s `requestActivation`) has nothing to do. `cli`'s
* real startup wiring (Task 1.15) passes the registry built alongside
* `host/registration.ts`'s `LoadExtensionsResult.pendingViews` and
* `host/activation.ts`'s `activateExtension`.
*/
slotRegistry?: SlotRegistry;
}

/**
Expand DownExpand Up@@ -144,14 +159,25 @@ export function createTecodeApi(deps: CreateTecodeApiDeps): Tecode {

const editorNamespace: EditorNamespace = Object.freeze(createEditorStub({ sink: deps.sink }));

const uiStub = createUiStub({ getTheme: () => themesNamespace.current });
// No slot registry injected (see CreateTecodeApiDeps.slotRegistry's
// TSDoc) — build one with no pending manifest views and no activation
// hook rather than falling back to a disposable-only stub; registerView
// still round-trips correctly, and callers that DO need lazy-view
// activation (the real CLI startup, Task 1.15) pass their own.
const slotRegistry = deps.slotRegistry ?? createSlotRegistry({});
const uiNamespace: UiNamespace = Object.freeze({
registerView: uiStub.registerView,
useTheme: uiStub.useTheme,
List: uiStub.List,
Tree: uiStub.Tree,
Input: uiStub.Input,
Tabs: uiStub.Tabs,
registerView: slotRegistry.registerView,
// A plain, non-hook getter (Req 10.1) — NOT the real React hook
// `ui/theme.ts` exports under the same conceptual name. See
// `ui/theme.ts`'s TSDoc ("Two different useThemes, deliberately") for
// why `tecode.ui.useTheme()` must stay callable from plain extension
// code (the contract test's fixture extension calls it from
// `activate(ctx)`, outside any React render).
useTheme: () => themesNamespace.current,
List,
Tree,
Input,
Tabs,
});

const languagesStub = createLanguagesStub();
Expand Down
3 changes: 0 additions & 3 deletions packages/core/src/api/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -12,12 +12,9 @@ export {
createEditorStub,
createLanguagesStub,
createThemesStub,
createUiStub,
createWindowStub,
type LanguagesStub,
type RegisteredView,
type ThemesStub,
type UiStub,
type WindowStub,
} from "./stubs";
export { registerTecodeAlias } from "./alias";
33 changes: 5 additions & 28 deletions packages/core/src/api/stubs.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,7 +5,6 @@ import {
createEditorStub,
createLanguagesStub,
createThemesStub,
createUiStub,
createWindowStub,
} from "./stubs";

Expand DownExpand Up@@ -126,33 +125,11 @@ test("editor stub: a throwing sink does not make revealLine/insertSnippet/applyE
expect(() => editor.applyEdits([])).not.toThrow();
});

test("ui.registerView: register/dispose symmetry", () => {
const ui = createUiStub({ getTheme: createBaseTheme });
const component = () => undefined;

const sub = ui.registerView("sidebar.view", "test.view", component);
expect(ui.registeredViews()).toEqual([{ slot: "sidebar.view", id: "test.view", component }]);

sub.dispose();
expect(ui.registeredViews()).toEqual([]);
expect(() => sub.dispose()).not.toThrow();
});

test("ui.useTheme delegates to the injected getTheme", () => {
const theme = createBaseTheme();
const ui = createUiStub({ getTheme: () => theme });

expect(ui.useTheme()).toBe(theme);
});

test("ui stub's List/Tree/Input/Tabs are inert placeholder components", () => {
const ui = createUiStub({ getTheme: createBaseTheme });

expect(ui.List({})).toBeUndefined();
expect(ui.Tree({})).toBeUndefined();
expect(ui.Input({})).toBeUndefined();
expect(ui.Tabs({})).toBeUndefined();
});
// `ui.registerView`/`useTheme`/`List`/`Tree`/`Input`/`Tabs` were stubbed
// here through Task 1.13; Task 1.14 gives them real backing instead (the
// slot registry, `ui/slotRegistry.test.ts`; the real components,
// `ui/components.test.tsx`) — see `stubs.ts`'s and `create.ts`'s TSDoc for
// the wiring.

test("languages.register: register/dispose symmetry, getLanguageId always 'plaintext'", () => {
const languages = createLanguagesStub();
Expand Down
58 changes: 5 additions & 53 deletions packages/core/src/api/stubs.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -19,11 +19,11 @@
* hardcoded base palette (design.md §12's own note that `ThemeProvider`
* starts with "a hardcoded base palette for now", Task 1.14) until a real
* theme loader can resolve one.
* - `ui.registerView` is likewise a real, disposable registration with no
* renderer behind it yet (the UI shell's slot registry, Task 1.14);
* `List`/`Tree`/`Input`/`Tabs` are inert placeholder components (no
* dependency on React here — `@tecode/api`'s `ComponentType` is
* deliberately framework-agnostic, design.md §12).
* - `ui` is no longer stubbed here as of Task 1.14: `tecode.ui.registerView`
* delegates to the real `ui/slotRegistry.ts` (a live, rendered slot
* registry, not just a disposable-returning placeholder), and `List`/
* `Tree`/`Input`/`Tabs` are the real OpenTUI/React components in
* `ui/components.ts` — see `create.ts` for the wiring.
*
* None of this throws: every method here follows the same never-throw
* discipline as the rest of core (`registry.ts`, `documentManager.ts`,
Expand All@@ -32,20 +32,17 @@
*/

import type {
ComponentType,
Disposable,
EditorNamespace,
LanguageContribution,
LanguagesNamespace,
Position,
ResolvedTheme,
RGB,
SlotId,
StatusBarItem,
ThemeContribution,
ThemesNamespace,
UiColorKey,
UiNamespace,
WindowNamespace,
} from "@tecode/api";
import type { StatusSink } from "../host/errors";
Expand DownExpand Up@@ -268,51 +265,6 @@ export function createEditorStub(deps: { sink: StatusSink }): EditorNamespace {
};
}

/** An inert placeholder `ComponentType` — `@tecode/api` has no dependency
* on React (or any UI framework, design.md §12), and no renderer exists
* yet to give `List`/`Tree`/`Input`/`Tabs` real behavior. */
const notImplementedComponent: ComponentType = () => undefined;

/** One registered `ui.registerView` call. */
export interface RegisteredView {
slot: SlotId;
id: string;
component: ComponentType;
}

/** {@link createUiStub}'s return type — see {@link WindowStub}'s TSDoc for
* why a stub factory returns more than its `@tecode/api` namespace type. */
export interface UiStub extends UiNamespace {
/** Every currently-registered view; an entry is gone once its
* `Disposable` has been disposed. */
registeredViews(): readonly RegisteredView[];
}

/**
* Build the `tecode.ui` stub (Req 10.1, 6.3). `registerView` is a real,
* disposable registration (the UI shell's slot registry, Task 1.14, is the
* eventual consumer); `useTheme` reads whatever `getTheme` currently
* returns, so it stays in sync with `tecode.themes.current` without this
* module depending on `themes.ts` directly (the two are wired together in
* `create.ts`).
*/
export function createUiStub(deps: { getTheme: () => ResolvedTheme }): UiStub {
const views = createRegistrySet<RegisteredView>();
return {
registerView(slot: SlotId, id: string, component: ComponentType) {
return views.register({ slot, id, component });
},
useTheme() {
return deps.getTheme();
},
List: notImplementedComponent,
Tree: notImplementedComponent,
Input: notImplementedComponent,
Tabs: notImplementedComponent,
registeredViews: views.entries,
};
}

/** {@link createLanguagesStub}'s return type — see {@link WindowStub}'s
* TSDoc for why a stub factory returns more than its `@tecode/api`
* namespace type. */
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/host/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -17,6 +17,7 @@ export {
getUserConfigDir,
getUserExtensionsDir,
getUserKeybindingsPath,
getUserLayoutStatePath,
getUserSettingsPath,
getWorkspaceExtensionsDir,
getWorkspaceSettingsPath,
Expand Down
5 changes: 5 additions & 0 deletions packages/core/src/host/paths.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,7 @@ import {
getUserConfigDir,
getUserExtensionsDir,
getUserKeybindingsPath,
getUserLayoutStatePath,
getUserSettingsPath,
getWorkspaceExtensionsDir,
getWorkspaceSettingsPath,
Expand DownExpand Up@@ -82,4 +83,8 @@ describe("derived file paths", () => {
join("/home/user/project", ".tecode", "extensions"),
);
});

test("getUserLayoutStatePath appends state.json to the config dir", () => {
expect(getUserLayoutStatePath()).toBe(join(getUserConfigDir(), "state.json"));
});
});
8 changes: 8 additions & 0 deletions packages/core/src/host/paths.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -35,6 +35,14 @@ export function getUserKeybindingsPath(): string {
return join(getUserConfigDir(), "keybindings.json");
}

/** Path to the user-level `state.json` — persisted UI layout state (sidebar
* width/visibility, panel height/visibility, active view — Req 6.4,
* design.md §8.2: "Layout state ... persists to `~/.config/tecode/state.json`
* on change (debounced) and on exit"). */
export function getUserLayoutStatePath(): string {
return join(getUserConfigDir(), "state.json");
}

/** Path to a workspace's `.tecode/settings.json`, overlaid on top of user
* settings when the workspace declares one (Req 9.2). `workspaceRoot` is
* the workspace's root directory (an absolute path). */
Expand Down
53 changes: 49 additions & 4 deletions packages/core/src/index.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -99,7 +99,55 @@ export {
createFileSystem,
type FileSystemDeps,
} from "./buffer/index";
export { UI_PLACEHOLDER } from "./ui/index";
export {
ActivityBar,
ContextFocusTracker,
createLayoutStateService,
createSlotRegistry,
DEFAULT_LAYOUT_STATE,
EditorArea,
Input,
List,
Panel,
RegisteredView,
Shell,
Sidebar,
StatusBar,
Tabs,
ThemeProvider,
toColorInput,
Tree,
styleToTextColors,
useFocusTracking,
useTheme,
type ActivityBarProps,
type ContextFocusTrackerProps,
type EditorAreaProps,
type FocusEmitter,
type InputProps,
type LayoutState,
type LayoutStateFs,
type LayoutStateService,
type LayoutStateServiceDeps,
type LayoutStateTimer,
type ListItem,
type ListProps,
type PanelProps,
type RegisterViewMeta,
type ShellProps,
type SidebarPair,
type SidebarProps,
type SlotRegistry,
type SlotRegistryDeps,
type SlotViewEntry,
type StatusBarPlacement,
type StatusBarProps,
type TabItem,
type TabsProps,
type ThemeProviderProps,
type TreeNode,
type TreeProps,
} from "./ui/index";
export {
createConfigService,
parseJsonc,
Expand All@@ -116,13 +164,10 @@ export {
createLanguagesStub,
createTecodeApi,
createThemesStub,
createUiStub,
createWindowStub,
registerTecodeAlias,
type CreateTecodeApiDeps,
type LanguagesStub,
type RegisteredView,
type ThemesStub,
type UiStub,
type WindowStub,
} from "./api/index";
Loading
Loading