Skip to content

Commit 1f617cc

Browse files
committed
Handle python notebook creation
1 parent 5a19d38 commit 1f617cc

2 files changed

Lines changed: 134 additions & 3 deletions

File tree

packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import assert from "assert";
2-
import {EventEmitter, TreeView} from "vscode";
2+
import {EventEmitter, TreeView, Uri, window, workspace} from "vscode";
33
import {mock, instance, when} from "ts-mockito";
44
import {WorkspaceFsCommands} from "./WorkspaceFsCommands";
55
import {WorkspaceFsEntity} from "../sdk-extensions";
@@ -134,7 +134,7 @@ describe("WorkspaceFsCommands – target folder resolution", () => {
134134
});
135135

136136
it("no element → targets root", async () => {
137-
await commands.createFile(undefined);
137+
await commands.createFile(makeEntity(ROOT_PATH));
138138
assert.strictEqual(capturedRootPath, ROOT_PATH);
139139
});
140140

@@ -243,3 +243,107 @@ describe("WorkspaceFsCommands – target folder resolution", () => {
243243
});
244244
});
245245
});
246+
247+
describe("WorkspaceFsCommands – createFile content", () => {
248+
const ROOT_PATH = "/Users/me";
249+
250+
let commands: WorkspaceFsCommands;
251+
let capturedCreate: {path: string; content: string} | undefined;
252+
253+
// Stubbed globals are restored after each test.
254+
let originalShowInputBox: typeof window.showInputBox;
255+
let originalShowTextDocument: typeof window.showTextDocument;
256+
let originalOpenTextDocument: typeof workspace.openTextDocument;
257+
let originalFromPath: typeof WorkspaceFsEntity.fromPath;
258+
259+
let inputName: string;
260+
261+
beforeEach(() => {
262+
const mockConnectionManager = mock<ConnectionManager>();
263+
when(mockConnectionManager.workspaceClient).thenReturn({} as any);
264+
265+
// createDirWizard reads activeProjectUri to seed the input box.
266+
const mockWorkspaceFolderManager = mock<WorkspaceFolderManager>();
267+
when(mockWorkspaceFolderManager.activeProjectUri).thenReturn(
268+
Uri.file("/tmp/project")
269+
);
270+
271+
commands = new WorkspaceFsCommands(
272+
instance(mockWorkspaceFolderManager),
273+
instance(mockConnectionManager),
274+
instance(mock<WorkspaceFsDataProvider>()),
275+
instance(mock<WorkspaceFsFileSystemProvider>()),
276+
new FakeTreeView() as unknown as TreeView<WorkspaceFsEntity>
277+
);
278+
279+
// Fake root that captures what doCreateFile writes.
280+
capturedCreate = undefined;
281+
const fakeRoot = {
282+
path: ROOT_PATH,
283+
createFile: async (path: string, content: string) => {
284+
capturedCreate = {path, content};
285+
},
286+
};
287+
(commands as any).getValidRoot = async () => fakeRoot;
288+
289+
// createDirWizard reads the filename from showInputBox.
290+
originalShowInputBox = window.showInputBox;
291+
(window as any).showInputBox = async () => inputName;
292+
293+
// No pre-existing file → skip the overwrite prompt.
294+
originalFromPath = WorkspaceFsEntity.fromPath;
295+
(WorkspaceFsEntity as any).fromPath = async () => undefined;
296+
297+
// Avoid actually opening an editor after creation.
298+
originalShowTextDocument = window.showTextDocument;
299+
(window as any).showTextDocument = async () => undefined;
300+
originalOpenTextDocument = workspace.openTextDocument;
301+
(workspace as any).openTextDocument = async (uri: Uri) => uri;
302+
});
303+
304+
afterEach(() => {
305+
(window as any).showInputBox = originalShowInputBox;
306+
(window as any).showTextDocument = originalShowTextDocument;
307+
(workspace as any).openTextDocument = originalOpenTextDocument;
308+
(WorkspaceFsEntity as any).fromPath = originalFromPath;
309+
});
310+
311+
it(".ipynb file is created with valid empty notebook JSON", async () => {
312+
inputName = "notebook.ipynb";
313+
await commands.createFile(makeEntity(ROOT_PATH));
314+
315+
assert.ok(capturedCreate, "createFile should have been called");
316+
assert.strictEqual(capturedCreate!.path, "notebook.ipynb");
317+
318+
const parsed = JSON.parse(capturedCreate!.content);
319+
assert.deepStrictEqual(parsed.cells, []);
320+
assert.strictEqual(parsed.nbformat, 4);
321+
assert.strictEqual(parsed.nbformat_minor, 5);
322+
assert.strictEqual(parsed.metadata.language_info.name, "python");
323+
});
324+
325+
it(".IPYNB extension is matched case-insensitively", async () => {
326+
inputName = "NoteBook.IPYNB";
327+
await commands.createFile(makeEntity(ROOT_PATH));
328+
329+
assert.ok(capturedCreate);
330+
const parsed = JSON.parse(capturedCreate!.content);
331+
assert.strictEqual(parsed.nbformat, 4);
332+
});
333+
334+
it(".py file is created with empty content", async () => {
335+
inputName = "script.py";
336+
await commands.createFile(makeEntity(ROOT_PATH));
337+
338+
assert.ok(capturedCreate);
339+
assert.strictEqual(capturedCreate!.content, "");
340+
});
341+
342+
it("plain file is created with empty content", async () => {
343+
inputName = "notes.txt";
344+
await commands.createFile(makeEntity(ROOT_PATH));
345+
346+
assert.ok(capturedCreate);
347+
assert.strictEqual(capturedCreate!.content, "");
348+
});
349+
});

packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,29 @@ import {WorkspaceFsFile} from "../sdk-extensions/wsfs/WorkspaceFsFile";
1616

1717
const withLogContext = logging.withLogContext;
1818

19+
/**
20+
* Minimal valid empty Python notebook (nbformat 4.5) used as the initial
21+
* content when creating a `.ipynb` file, so it can be opened as a notebook
22+
* right away instead of as an invalid/empty document.
23+
*/
24+
const EMPTY_IPYNB_CONTENT = JSON.stringify(
25+
{
26+
cells: [],
27+
metadata: {
28+
kernelspec: {
29+
display_name: "Python 3",
30+
language: "python",
31+
name: "python3",
32+
},
33+
language_info: {name: "python"},
34+
},
35+
nbformat: 4,
36+
nbformat_minor: 5,
37+
},
38+
null,
39+
1
40+
);
41+
1942
export class WorkspaceFsCommands implements Disposable {
2043
private disposables: Disposable[] = [];
2144

@@ -202,8 +225,12 @@ export class WorkspaceFsCommands implements Disposable {
202225
}
203226
}
204227

228+
const content = inputName.toLowerCase().endsWith(".ipynb")
229+
? EMPTY_IPYNB_CONTENT
230+
: "";
231+
205232
try {
206-
await root.createFile(inputName, "", true);
233+
await root.createFile(inputName, content, true);
207234
} catch (e: unknown) {
208235
const msg = e instanceof Error ? e.message : String(e);
209236
window.showErrorMessage(`Failed to create "${inputName}": ${msg}`);

0 commit comments

Comments
 (0)