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
4 changes: 4 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"clientKind": "git",
"useIgnoreFile": true
},
"plugins": ["./extension/lint/vscode-type-only.grit"],
"files": {
"ignoreUnknown": false
},
Expand All @@ -18,6 +19,9 @@
"recommended": true,
"correctness": {
"useHookAtTopLevel": "off"
},
"style": {
"useImportType": "error"
}
}
},
Expand Down
13 changes: 13 additions & 0 deletions extension/lint/vscode-type-only.grit
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
language js

or {
`import * as $n from "vscode"` where {
register_diagnostic(span=$n, message="Use type-only imports for vscode module. Change to: import type * as ... from 'vscode'", severity="error")
},
`import { $names } from "vscode"` where {
register_diagnostic(span=$names, message="Use type-only imports for vscode module. Change to: import type { ... } from 'vscode'", severity="error")
},
`import $name from "vscode"` where {
register_diagnostic(span=$name, message="Use type-only imports for vscode module. Change to: import type ... from 'vscode'", severity="error")
}
}
186 changes: 174 additions & 12 deletions extension/src/__mocks__/vscode.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { type ChildProcess, spawn } from "node:child_process";
import { createVSCodeMock as create } from "jest-mock-vscode";
import { afterAll, type VitestUtils } from "vitest";
import type { NotebookController } from "vscode";
import * as NodeChildProcess from "node:child_process";
import * as jestMockVscode from "jest-mock-vscode";
import * as vitest from "vitest";
// biome-ignore lint: we need type-only import
import type * as vscode from "vscode";

const openProcesses: ChildProcess[] = [];
const openProcesses: NodeChildProcess.ChildProcess[] = [];

export async function createVSCodeMock(vi: VitestUtils) {
export async function createVSCodeMock(vi: vitest.VitestUtils) {
// biome-ignore lint/suspicious/noExplicitAny: any is ok
const vscode = create(vi) as any;
const vscode = jestMockVscode.createVSCodeMock(vi) as any;

vscode.workspace = vscode.workspace || {};
let configMap: Record<string, unknown> = {};
Expand All @@ -18,14 +19,14 @@ export async function createVSCodeMock(vi: VitestUtils) {

// Add createTerminal mock
vscode.window.createTerminal = vi.fn().mockImplementation(() => {
let proc: ChildProcess | undefined;
let proc: NodeChildProcess.ChildProcess | undefined;
return {
processId: Promise.resolve(1),
dispose: vi.fn().mockImplementation(() => {
proc?.kill();
}),
sendText: vi.fn().mockImplementation((args: string) => {
proc = spawn(args, { shell: true });
proc = NodeChildProcess.spawn(args, { shell: true });
proc.stdout?.on("data", (data) => {
const line = data.toString();
if (line) {
Expand Down Expand Up @@ -102,7 +103,7 @@ export async function createVSCodeMock(vi: VitestUtils) {
vscode.notebooks.createNotebookController = vi
.fn()
.mockImplementation((id, notebookType, label) => {
const mockNotebookController: NotebookController = {
const mockNotebookController: vscode.NotebookController = {
id,
notebookType,
supportedLanguages: [],
Expand All @@ -114,7 +115,7 @@ export async function createVSCodeMock(vi: VitestUtils) {
onDidChangeSelectedNotebooks: vi.fn(),
updateNotebookAffinity: vi.fn(),
dispose: vi.fn(),
};
} satisfies vscode.NotebookController;
return mockNotebookController;
});

Expand All @@ -125,12 +126,173 @@ export async function createVSCodeMock(vi: VitestUtils) {
}),
});

vscode.debug = vscode.debug || {};
vscode.debug.registerDebugConfigurationProvider = vi.fn();
vscode.debug.registerDebugAdapterDescriptorFactory = vi.fn().mockReturnValue({
dispose: vi.fn(),
});

// Add missing data type constructors that VsCode service exports
vscode.NotebookData = class NotebookData implements vscode.NotebookData {
cells: Array<vscode.NotebookCellData>;
constructor(cells: Array<vscode.NotebookCellData>) {
this.cells = cells;
}
};

vscode.NotebookCellData = class NotebookCellData
implements vscode.NotebookCellData
{
kind: vscode.NotebookCellKind;
value: string;
languageId: string;
constructor(
kind: vscode.NotebookCellKind,
value: string,
languageId: string,
) {
this.kind = kind;
this.value = value;
this.languageId = languageId;
}
};

vscode.NotebookCellKind = {
Markup: 1,
Code: 2,
};

vscode.NotebookCellOutput = class NotebookCellOutput
implements vscode.NotebookCellOutput
{
items: Array<vscode.NotebookCellOutputItem>;
metadata: { [key: string]: unknown };
constructor(
items: Array<vscode.NotebookCellOutputItem>,
metadata?: { [key: string]: unknown },
) {
this.items = items;
this.metadata = metadata ?? {};
}
};

vscode.NotebookCellOutputItem = class NotebookCellOutputItem
implements vscode.NotebookCellOutputItem
{
static text(value: string, mime?: string): NotebookCellOutputItem {
return new NotebookCellOutputItem(
new TextEncoder().encode(value),
mime || "text/plain",
);
}
static json(value: unknown, mime?: string): NotebookCellOutputItem {
return new NotebookCellOutputItem(
new TextEncoder().encode(JSON.stringify(value)),
mime || "application/json",
);
}
static stdout(value: string): NotebookCellOutputItem {
return new NotebookCellOutputItem(
new TextEncoder().encode(value),
"application/vnd.code.notebook.stdout",
);
}
static stderr(value: string): NotebookCellOutputItem {
return new NotebookCellOutputItem(
new TextEncoder().encode(value),
"application/vnd.code.notebook.stderr",
);
}
static error(value: Error): NotebookCellOutputItem {
return new NotebookCellOutputItem(
new TextEncoder().encode(
JSON.stringify({
name: value.name,
message: value.message,
stack: value.stack,
}),
),
"application/vnd.code.notebook.error",
);
}

mime: string;
data: Uint8Array;
constructor(data: Uint8Array, mime: string) {
this.data = data;
this.mime = mime;
}
};

vscode.EventEmitter = class EventEmitter<T>
implements vscode.EventEmitter<T>
{
#listeners: Array<(e: T) => unknown> = [];
event: vscode.Event<T> = (listener) => {
this.#listeners.push(listener);
return {
dispose: () =>
this.#listeners.splice(this.#listeners.indexOf(listener), 1),
};
};
fire(data: T) {
for (const listener of this.#listeners) {
listener(data);
}
}
dispose() {
this.#listeners = [];
}
};

vscode.DebugAdapterInlineImplementation = class DebugAdapterInlineImplementation
implements vscode.DebugAdapterInlineImplementation
{
implementation: vscode.DebugAdapter;
constructor(implementation: vscode.DebugAdapter) {
this.implementation = implementation;
}
};

// Add workspace properties using defineProperty for read-only properties
// Add workspace properties
Object.defineProperty(vscode.workspace, "notebookDocuments", {
value: [],
writable: false,
configurable: true,
});
Object.defineProperty(vscode.workspace, "workspaceFolders", {
value: [],
writable: false,
configurable: true,
});

// Add window properties
vscode.window.activeNotebookEditor = undefined;

// Add env.openExternal
vscode.env.openExternal = vi.fn().mockResolvedValue(true);

// Add commands.executeCommand
vscode.commands = vscode.commands || {};
vscode.commands.executeCommand = vi.fn().mockResolvedValue(undefined);

// Add Uri.parse
vscode.Uri = vscode.Uri || {};
vscode.Uri.parse = vi.fn().mockImplementation((value) => ({
scheme: "https",
authority: "",
path: value,
query: "",
fragment: "",
fsPath: value,
toString: () => value,
}));

return vscode;
}

afterAll(() => {
vitest.afterAll(() => {
for (const proc of openProcesses) {
proc.kill();
}
Expand Down
7 changes: 2 additions & 5 deletions extension/src/extension.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Effect, Exit, Layer, Logger, LogLevel, pipe, Scope } from "effect";
import * as vscode from "vscode";
import type * as vscode from "vscode";

import { MainLive } from "./layers/Main.ts";

Expand All @@ -9,10 +9,7 @@ export async function activate(
return pipe(
Effect.gen(function* () {
yield* Effect.logInfo("Activating marimo extension").pipe(
Effect.annotateLogs({
extensionPath: context.extensionPath,
workspaceFolder: vscode.workspace.workspaceFolders?.[0]?.uri.fsPath,
}),
Effect.annotateLogs({ extensionPath: context.extensionPath }),
);
// Create a scope and build layers with it. Layer.buildWithScope completes
// once all layer initialization finishes (commands registered, serializer
Expand Down
7 changes: 4 additions & 3 deletions extension/src/layers/KernelManager.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { Effect, FiberSet, Layer, Option } from "effect";
import * as vscode from "vscode";
import { assert } from "../assert.ts";
import * as ops from "../operations.ts";
import { LanguageClient } from "../services/LanguageClient.ts";
import { NotebookControllers } from "../services/NotebookControllers.ts";
import { NotebookRenderer } from "../services/NotebookRenderer.ts";
import { VsCode } from "../services/VsCode.ts";

/**
* Orchestrates kernel operations for marimo notebooks by composing
Expand All @@ -19,6 +19,7 @@ export const KernelManagerLive = Layer.scopedDiscard(
yield* Effect.logInfo("Setting up kernel manager").pipe(
Effect.annotateLogs({ component: "kernel-manager" }),
);
const code = yield* VsCode;
const marimo = yield* LanguageClient;
const renderer = yield* NotebookRenderer;
const controllers = yield* NotebookControllers;
Expand All @@ -28,7 +29,7 @@ export const KernelManagerLive = Layer.scopedDiscard(
Omit<ops.OperationContext, "controller" | "renderer">
>();

const runFork = yield* FiberSet.makeRuntime<NotebookRenderer>();
const runFork = yield* FiberSet.makeRuntime<NotebookRenderer | VsCode>();

yield* marimo.onNotification("marimo/operation", (msg) =>
runFork(
Expand All @@ -37,7 +38,7 @@ export const KernelManagerLive = Layer.scopedDiscard(
let context = contexts.get(notebookUri);

if (!context) {
const notebook = vscode.workspace.notebookDocuments.find(
const notebook = code.workspace.notebookDocuments.find(
(doc) => doc.uri.toString() === notebookUri,
);
assert(notebook, `Expected notebook document for ${notebookUri}`);
Expand Down
13 changes: 11 additions & 2 deletions extension/src/layers/RegisterCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,17 @@ export const RegisterCommandsLive = Layer.scopedDiscard(
yield* code.commands.registerCommand(
"marimo.newMarimoNotebook",
Effect.gen(function* () {
const doc = yield* code.workspace.createEmptyPythonNotebook(
serializer.notebookType,
const doc = yield* code.workspace.use((api) =>
api.openNotebookDocument(
serializer.notebookType,
new code.NotebookData([
new code.NotebookCellData(
code.NotebookCellKind.Code,
"",
"python",
),
]),
),
);
yield* code.window.use((api) => api.showNotebookDocument(doc));
yield* Effect.logInfo("Created new marimo notebook").pipe(
Expand Down
Loading