Skip to content

Commit 177e43c

Browse files
feat(Cocoon): Implement core extension host services and user command workflow
- Added `ProcessUserData` command effect with structured error handling, demonstrating Effect-TS integration for extension workflows - Established core service layers (`ApiFactory`, `RequireInterceptor`, `ExtensionHost`) using Effect-TS Layer system to compose VS Code API surface - Implemented service definitions for 30+ VS Code API areas (Commands, LanguageFeatures, Documents) with gRPC IPC to Mountain backend - Introduced type-safe RPC handlers for language features like Hover providers, ensuring compatibility with `vine.proto` contracts - Configured process patching and module interception to secure extension environment while maintaining VS Code compatibility This foundational work enables Cocoon's Node.js extension host (Path A MVP) to provide full VS Code API compatibility while delegating privileged operations to Mountain via gRPC. Services are built as pure Effect layers, aligning with Land's declarative architecture and preparing for future Grove integration.
1 parent 7392fb5 commit 177e43c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

82 files changed

+2540
-10
lines changed

Source/Command/ProcessUserData.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* @module ProcessUserData (Command)
3+
* @description The main module for the 'ProcessUserData' command.
4+
*
5+
* This orchestrates the entire workflow of getting text from the active
6+
* editor, sending it to a backend service for processing, and then
7+
* displaying the result to the user.
8+
*/
9+
10+
import { Effect, pipe } from "effect";
11+
12+
import { InvokeProcessingService } from "../../Service/Mountain/InvokeProcessingService.js";
13+
import { GetActiveTextEditor } from "../../Service/Window/GetActiveTextEditor.js";
14+
import {
15+
ShowErrorMessage,
16+
ShowInformationMessage,
17+
} from "../../Service/Window/mod.js";
18+
import { ActiveEditorNotFoundError, ProcessingServiceError } from "./Error.js";
19+
import { GetDocumentText } from "./GetDocumentText.js";
20+
21+
/**
22+
* An `Effect` that encapsulates the entire workflow for processing user data
23+
* from the active text editor, demonstrating declarative, type-safe error
24+
* handling.
25+
*/
26+
export const ProcessUserData = pipe(
27+
Effect.gen(function* (_) {
28+
// Safely get the active editor, which may not exist.
29+
const MaybeEditor = yield* _(GetActiveTextEditor);
30+
31+
// Convert the Option into an Effect that fails with our specific error if empty.
32+
const Editor = yield* _(
33+
MaybeEditor,
34+
Effect.mapError(() => new ActiveEditorNotFoundError()),
35+
);
36+
37+
// If the above succeeds, the workflow proceeds.
38+
const TextContent = yield* _(GetDocumentText(Editor.document));
39+
const ProcessingResult = yield* _(InvokeProcessingService(TextContent));
40+
yield* _(
41+
ShowInformationMessage(
42+
`Processing complete: ${ProcessingResult.Id}`,
43+
),
44+
);
45+
}),
46+
// Declaratively handle all known, tagged failure cases for this workflow.
47+
Effect.catchTags({
48+
ActiveEditorNotFoundError: (Error) => ShowErrorMessage(Error.message),
49+
ProcessingServiceError: (Error) => ShowErrorMessage(Error.message),
50+
}),
51+
// Catch any other unexpected error that might have occurred, safely handling
52+
// the error type.
53+
Effect.catchAll((Error) =>
54+
ShowErrorMessage(
55+
`An unexpected error occurred: ${Error instanceof Error ? Error.message : String(Error)}`,
56+
),
57+
),
58+
);

Source/Core.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
// --- Creating a single, composed layer for all core services ---
2+
3+
import { Layer } from "effect";
4+
5+
import { Live as LiveApiFactory } from "./ApiFactory/mod.js";
6+
import { Live as LiveExtensionHost } from "./ExtensionHost/mod.js";
7+
import { Live as LiveExtensionPaths } from "./ExtensionPath/mod.js";
8+
import { Live as LiveRequireInterceptor } from "./RequireInterceptor/mod.js";
9+
10+
/**
11+
* @module Core
12+
* @description This is the aggregator module for all core services of the Cocoon
13+
* extension host. These services are fundamental to the runtime's operation,
14+
* managing the extension lifecycle, API creation, and module loading.
15+
*/
16+
17+
// --- Re-exporting the full public API (Tag, Interface, Live Layer) for each core service ---
18+
19+
export * as ApiFactory from "./ApiFactory/mod.js";
20+
export * as ExtensionHost from "./ExtensionHost/mod.js";
21+
export * as ExtensionPaths from "./ExtensionPath/mod.js";
22+
export * as RequireInterceptor from "./RequireInterceptor/mod.js";
23+
24+
/**
25+
* A single, composed layer that provides all core services of the extension host.
26+
* This simplifies the process of building the final application layer in `Index.ts`.
27+
*/
28+
export const CoreServicesLayer = Layer.mergeAll(
29+
LiveApiFactory,
30+
LiveExtensionHost,
31+
LiveExtensionPaths,
32+
LiveRequireInterceptor,
33+
);

Source/Core/APIFactory.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/**
2+
* @module ApiFactory
3+
* @description The main module for the `ApiFactory` service, which is
4+
* responsible for creating sandboxed `vscode` API objects for extensions.
5+
*/
6+
7+
import { Context, Effect, Layer } from "effect";
8+
import type { IExtensionDescription } from "vs/platform/extensions/common/extensions.js";
9+
import type * as Vscode from "vscode";
10+
11+
import * as Service from "../../Service/mod.js";
12+
import { CreateApiFactory } from "./CreateApiFactory.js";
13+
14+
/**
15+
* The interface for the `ApiFactory` service.
16+
*/
17+
export interface Interface {
18+
/**
19+
* Creates a new, sandboxed `vscode` API object for a specific extension.
20+
* @param Extension The full description of the extension requesting the API.
21+
* @returns A frozen `vscode` API object tailored for the extension.
22+
*/
23+
readonly Create: (Extension: IExtensionDescription) => typeof Vscode;
24+
}
25+
26+
/**
27+
* The `Context.Tag` for the `ApiFactory` service.
28+
*/
29+
export const Tag = Context.Tag<Interface>("ApiFactory");
30+
31+
/**
32+
* The live implementation `Layer` for the `ApiFactory` service.
33+
*
34+
* This layer has a comprehensive dependency graph, as it requires every
35+
* underlying service that contributes to the final `vscode` API object. It
36+
* injects all of these services into the `CreateApiFactory` function to
37+
* construct the final service implementation.
38+
*/
39+
export const Live = Layer.effect(
40+
Tag,
41+
Effect.gen(function* (_) {
42+
// --- Inject all necessary services ---
43+
const LogService = yield* _(Service.Log.Tag);
44+
const ProposedApiService = yield* _(Service.ProposedApi.Tag);
45+
const DeprecationService = yield* _(Service.ApiDeprecation.Tag);
46+
const CommandsService = yield* _(Service.Commands.Tag);
47+
const WorkspaceService = yield* _(Service.Workspace.Tag);
48+
const WindowService = yield* _(Service.Window.Tag);
49+
const LanguageFeaturesService = yield* _(Service.LanguageFeatures.Tag);
50+
const DebugService = yield* _(Service.Debug.Tag);
51+
const TasksService = yield* _(Service.Tasks.Tag);
52+
const ExtensionService = yield* _(Service.Extension.Tag);
53+
const WebviewPanelService = yield* _(Service.WebviewPanel.Tag);
54+
const CustomEditorService = yield* _(Service.CustomEditor.Tag);
55+
const TreeViewService = yield* _(Service.TreeView.Tag);
56+
const StatusBarService = yield* _(Service.StatusBar.Tag);
57+
// Add other services here as they are implemented.
58+
59+
// --- Construct the factory with all its dependencies ---
60+
return CreateApiFactory(
61+
LogService,
62+
ProposedApiService,
63+
DeprecationService,
64+
CommandsService,
65+
WorkspaceService,
66+
WindowService,
67+
LanguageFeaturesService,
68+
DebugService,
69+
TasksService,
70+
ExtensionService,
71+
WebviewPanelService,
72+
CustomEditorService,
73+
TreeViewService,
74+
StatusBarService,
75+
);
76+
}),
77+
);

Source/Core/ESMInterceptor.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @module EsmInterceptor (Core)
3+
* @description The main module for the EsmInterceptor service, which installs a
4+
* Node.js loader hook to intercept `import 'vscode'` statements.
5+
*/
6+
7+
import { Layer } from "effect";
8+
9+
import { Live as LiveLog } from "../../Service/Log.js";
10+
import { Live as LiveApiFactory } from "../ApiFactory/mod.js";
11+
import { Definition } from "./Definition.js";
12+
import { Tag } from "./Service.js";
13+
14+
/**
15+
* The live implementation layer for the EsmInterceptor service.
16+
* It depends on the ApiFactory and logging services.
17+
*/
18+
export const Live = Layer.effect(Tag, Definition).pipe(
19+
Layer.provide(Layer.merge(LiveApiFactory, LiveLog)),
20+
);

Source/Core/ExtensionHost.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @module ExtensionHost (Core)
3+
* @description The main module for the Extension Host service, which manages the
4+
* entire lifecycle of extensions.
5+
*/
6+
7+
import { Layer } from "effect";
8+
9+
import { InitDataService } from "../../Service/InitData.js";
10+
import { Live as LiveIpc } from "../../Service/Ipc/mod.js";
11+
import { Live as LiveLog } from "../../Service/Log.js";
12+
import { Live as LiveApiFactory } from "../ApiFactory/mod.js";
13+
import { Definition } from "./Definition.js";
14+
import { Tag } from "./Service.js";
15+
16+
/**
17+
* The live implementation layer for the ExtensionHost service.
18+
* It depends on the ApiFactory, logging, IPC, and initialization data services.
19+
*/
20+
export const Live = Layer.effect(Tag, Definition).pipe(
21+
Layer.provide(LiveApiFactory),
22+
Layer.provide(LiveLog),
23+
Layer.provide(LiveIpc),
24+
Layer.provide(Layer.succeed(InitDataService, {} as any)), // Placeholder for real init data
25+
);

Source/Core/ExtensionHost/Definition.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { ExtensionDescriptionRegistry } from "vs/workbench/services/extensions/c
99
import { InitDataService } from "../../Service/InitData.js";
1010
import { IpcProvider } from "../../Service/Ipc.js";
1111
import { LogProvider } from "../../Service/Log.js";
12-
import { ApiFactoryProvider } from "../ApiFactory.js";
12+
import { ApiFactoryProvider } from "../APIFactory.js";
1313
import { type Interface } from "./Service.js";
1414
import type { ActivatedExtension } from "./State.js";
1515

Source/Core/ExtensionPath.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
/**
2+
* @module ExtensionPaths (Core)
3+
* @description The main module for the ExtensionPaths service, which maps file URIs
4+
* to their owner extension.
5+
*/
6+
7+
import { Context, Effect, Layer } from "effect";
8+
9+
import { InitDataService } from "../../Service/InitData.js";
10+
import { Definition } from "./Definition.js";
11+
12+
/**
13+
* The interface for the ExtensionPaths service is the class definition itself.
14+
*/
15+
export type Interface = Definition;
16+
17+
/**
18+
* The Context.Tag for the ExtensionPaths service.
19+
*/
20+
export const Tag = Context.Tag<Interface>("Core/ExtensionPaths");
21+
22+
/**
23+
* The live implementation layer for the ExtensionPaths service.
24+
* It depends on the InitDataService to get the list of all installed extensions.
25+
*/
26+
export const Live = Layer.effect(
27+
Tag,
28+
Effect.map(
29+
InitDataService,
30+
(InitData) => new Definition(InitData.extensions),
31+
),
32+
);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/**
2+
* @module Definition (ExtensionPaths)
3+
* @description The class implementation for the ExtensionPaths service.
4+
*/
5+
6+
import { URI } from "vs/base/common/uri.js";
7+
import {
8+
ExtensionIdentifier,
9+
type IExtensionDescription,
10+
} from "vs/platform/extensions/common/extensions.js";
11+
12+
interface ExtensionPathsEntry {
13+
readonly Path: string;
14+
readonly Identifier: ExtensionIdentifier;
15+
}
16+
17+
/**
18+
* A service that maintains an index of installed extension paths, allowing for
19+
* quick, synchronous lookups from a file URI to its owner extension.
20+
* This is a critical dependency for the `RequireInterceptor`.
21+
*/
22+
export class Definition {
23+
private readonly Paths: readonly ExtensionPathsEntry[];
24+
25+
constructor(Extensions: readonly IExtensionDescription[]) {
26+
const mutablePaths: ExtensionPathsEntry[] = [];
27+
for (const Extension of Extensions) {
28+
if (Extension.extensionLocation) {
29+
mutablePaths.push({
30+
Path: URI.revive(Extension.extensionLocation).fsPath,
31+
Identifier: Extension.identifier,
32+
});
33+
}
34+
}
35+
36+
// Sort by path length, longest first. This is crucial to ensure that if
37+
// one extension's folder is inside another's, the more specific
38+
// (longer) path is matched first.
39+
mutablePaths.sort((a, b) => b.Path.length - a.Path.length);
40+
this.Paths = mutablePaths;
41+
}
42+
43+
/**
44+
* Finds the extension description that corresponds to a given file URI by
45+
* checking if the URI's path is a substring of any known extension path.
46+
*
47+
* @param Uri - The file URI to look up.
48+
* @returns A minimal `IExtensionDescription` containing the identifier if a
49+
* match is found, otherwise `undefined`.
50+
*/
51+
public FindSubstr(Uri: URI): IExtensionDescription | undefined {
52+
const FilePath = Uri.fsPath;
53+
for (const Entry of this.Paths) {
54+
if (FilePath.startsWith(Entry.Path)) {
55+
// Return a minimal description, as this is all the interceptor needs.
56+
return {
57+
identifier: Entry.Identifier,
58+
} as IExtensionDescription;
59+
}
60+
}
61+
return undefined;
62+
}
63+
}

Source/Core/HostKindPicker.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* @module HostKindPicker (Core)
3+
* @description This module provides the HostKindPicker service, which determines if
4+
* an extension is compatible with the Cocoon (Node.js) extension host.
5+
*/
6+
7+
import { Layer } from "effect";
8+
9+
import { Live as LiveLog } from "../../Service/Log.js";
10+
import { Definition } from "./Definition.js";
11+
import { Tag } from "./Service.js";
12+
13+
export { Tag, type Interface } from "./Service.js";
14+
15+
/**
16+
* The live implementation Layer for the HostKindPicker service.
17+
* It depends on the Log service for reporting its decisions.
18+
*/
19+
export const Live = Layer.effect(Tag, Definition).pipe(Layer.provide(LiveLog));

Source/Core/NodeModuleShim.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* @module NodeModuleShim (Core)
3+
* @description Provides the NodeModuleShim service, which intercepts requests for
4+
* built-in Node.js modules, blocking some and providing safe shims for others.
5+
*/
6+
7+
import { Layer } from "effect";
8+
9+
import { Live as LiveLog } from "../../Service/Log.js";
10+
import { Definition } from "./Definition.js";
11+
import { Tag } from "./Service.js";
12+
13+
export { Tag, type Interface } from "./Service.js";
14+
export * from "./Error.js";
15+
16+
/**
17+
* The live implementation Layer for the NodeModuleShim service.
18+
* It depends on the Log service for reporting interception events.
19+
*/
20+
export const Live = Layer.effect(Tag, Definition).pipe(Layer.provide(LiveLog));

0 commit comments

Comments
 (0)