From 5a19d3808c11dd20c2d4d05afd61e33dcf5065dd Mon Sep 17 00:00:00 2001 From: misha-db Date: Mon, 22 Jun 2026 17:19:43 +0400 Subject: [PATCH 1/7] Add create file support to WSFS explorer --- packages/databricks-vscode/package.json | 26 +++++- packages/databricks-vscode/src/extension.ts | 10 +++ .../workspace-fs/WorkspaceFsCommands.test.ts | 53 ++++++++++++ .../src/workspace-fs/WorkspaceFsCommands.ts | 85 +++++++++++++++++++ 4 files changed, 173 insertions(+), 1 deletion(-) diff --git a/packages/databricks-vscode/package.json b/packages/databricks-vscode/package.json index cb6d6bbe0..2275f8dc6 100644 --- a/packages/databricks-vscode/package.json +++ b/packages/databricks-vscode/package.json @@ -196,6 +196,20 @@ "enablement": "databricks.context.activated && databricks.context.loggedIn && !databricks.context.remoteMode", "category": "Databricks" }, + { + "command": "databricks.wsfs.createNewFile", + "title": "Create File", + "icon": "$(new-file)", + "enablement": "databricks.context.activated && databricks.context.loggedIn && !databricks.context.remoteMode", + "category": "Databricks" + }, + { + "command": "databricks.wsfs.createNewFile.toolbar", + "title": "Create File", + "icon": "$(new-file)", + "enablement": "databricks.context.activated && databricks.context.loggedIn && !databricks.context.remoteMode", + "category": "Databricks" + }, { "command": "databricks.wsfs.openInBrowser", "title": "Open in Browser", @@ -726,6 +740,11 @@ "when": "view == workspaceFsView", "group": "navigation@1" }, + { + "command": "databricks.wsfs.createNewFile.toolbar", + "when": "view == workspaceFsView", + "group": "navigation@1" + }, { "command": "databricks.unityCatalog.filter", "when": "view == unityCatalogView", @@ -803,10 +822,15 @@ "group": "wsfs_mut@0" }, { - "command": "databricks.wsfs.uploadFile", + "command": "databricks.wsfs.createNewFile", "when": "view == workspaceFsView && (viewItem == wsfs.directory || viewItem == wsfs.repo)", "group": "wsfs_mut@1" }, + { + "command": "databricks.wsfs.uploadFile", + "when": "view == workspaceFsView && (viewItem == wsfs.directory || viewItem == wsfs.repo)", + "group": "wsfs_mut@2" + }, { "command": "databricks.wsfs.downloadFile", "when": "view == workspaceFsView && (viewItem == wsfs.file || viewItem == wsfs.notebook)", diff --git a/packages/databricks-vscode/src/extension.ts b/packages/databricks-vscode/src/extension.ts index 69ca1fc94..995255a63 100644 --- a/packages/databricks-vscode/src/extension.ts +++ b/packages/databricks-vscode/src/extension.ts @@ -429,6 +429,16 @@ export async function activate( workspaceFsCommands.createFolderFromToolbar, workspaceFsCommands ), + telemetry.registerCommand( + "databricks.wsfs.createNewFile", + workspaceFsCommands.createFile, + workspaceFsCommands + ), + telemetry.registerCommand( + "databricks.wsfs.createNewFile.toolbar", + workspaceFsCommands.createFileFromToolbar, + workspaceFsCommands + ), telemetry.registerCommand( "databricks.wsfs.openInBrowser", workspaceFsCommands.openInBrowser, diff --git a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts index 3c7c197c7..90187e92b 100644 --- a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts +++ b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts @@ -126,6 +126,59 @@ describe("WorkspaceFsCommands – target folder resolution", () => { }); }); + // createFile mirrors createFolder's target resolution but, like upload, + // checks workspaceClient before reaching getValidRoot. + describe("createFile (context menu)", () => { + beforeEach(() => { + when(mockConnectionManager.workspaceClient).thenReturn({} as any); + }); + + it("no element → targets root", async () => { + await commands.createFile(undefined); + assert.strictEqual(capturedRootPath, ROOT_PATH); + }); + + it("element=A → targets A", async () => { + await commands.createFile(entityA); + assert.strictEqual(capturedRootPath, entityA.path); + }); + + it("element=B while A is selected → targets B", async () => { + fakeTreeView.simulateSelect(entityA); + await commands.createFile(entityB); + assert.strictEqual(capturedRootPath, entityB.path); + }); + }); + + describe("createFileFromToolbar (toolbar)", () => { + beforeEach(() => { + when(mockConnectionManager.workspaceClient).thenReturn({} as any); + }); + + it("nothing selected → targets root", async () => { + await commands.createFileFromToolbar(undefined); + assert.strictEqual(capturedRootPath, ROOT_PATH); + }); + + it("A selected, toolbar clicked → targets A", async () => { + fakeTreeView.simulateSelect(entityA); + await commands.createFileFromToolbar(entityA); + assert.strictEqual(capturedRootPath, entityA.path); + }); + + it("selection cleared before toolbar click → targets root", async () => { + fakeTreeView.simulateSelect(entityA); + fakeTreeView.simulateSelect(undefined); + await commands.createFileFromToolbar(undefined); + assert.strictEqual(capturedRootPath, ROOT_PATH); + }); + + it("element passed but nothing selected (edge case) → targets root", async () => { + await commands.createFileFromToolbar(entityA); + assert.strictEqual(capturedRootPath, ROOT_PATH); + }); + }); + describe("uploadFile (context menu)", () => { // doUploadFile checks workspaceClient before reaching getValidRoot; // provide a non-null client so root-path resolution is exercised. diff --git a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts index 4caac439e..b39766a6d 100644 --- a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts +++ b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts @@ -141,6 +141,91 @@ export class WorkspaceFsCommands implements Disposable { return created; } + @withLogContext(Loggers.Extension) + async createFile(element?: WorkspaceFsEntity, @context ctx?: Context) { + const rootPath = + element?.path ?? + this.connectionManager.databricksWorkspace?.currentFsRoot.path; + return this.doCreateFile(rootPath, ctx); + } + + @withLogContext(Loggers.Extension) + async createFileFromToolbar( + element?: WorkspaceFsEntity, + @context ctx?: Context + ) { + const activeElement = this.resolveTargetElementForToolbar(element); + const rootPath = + activeElement?.path ?? + this.connectionManager.databricksWorkspace?.currentFsRoot.path; + return this.doCreateFile(rootPath, ctx); + } + + @withLogContext(Loggers.Extension) + private async doCreateFile( + rootPath: string | undefined, + @context ctx?: Context + ) { + const client = this.connectionManager.workspaceClient; + if (!client) { + window.showErrorMessage("Please login first to create a file"); + return; + } + + const root = await this.getValidRoot(rootPath, ctx); + if (!root) { + return; + } + + const inputName = await createDirWizard( + this.workspaceFolderManager.activeProjectUri, + "File Name", + root + ); + + if (inputName === undefined) { + return; + } + + const existing = await WorkspaceFsEntity.fromPath( + client, + `${root.path}/${inputName}` + ); + if (existing) { + const answer = await window.showWarningMessage( + `"${inputName}" already exists in the workspace. Overwrite it?`, + {modal: true}, + "Overwrite" + ); + if (answer !== "Overwrite") { + return; + } + } + + try { + await root.createFile(inputName, "", true); + } catch (e: unknown) { + const msg = e instanceof Error ? e.message : String(e); + window.showErrorMessage(`Failed to create "${inputName}": ${msg}`); + return; + } + + this.workspaceFsDataProvider.refresh(); + const uri = Uri.from({ + scheme: WorkspaceFsFileSystemProvider.scheme, + path: `${root.path}/${inputName}`, + }); + this.fsp.notifyCreated(uri); + + try { + await window.showTextDocument( + await workspace.openTextDocument(uri) + ); + } catch (e: unknown) { + ctx?.logger?.error(`Can't open ${inputName} after creation`, e); + } + } + private async createRepo(repoPath: string) { const wsClient = this.connectionManager.workspaceClient; if (!wsClient) { From 1f617ccdaa3d4eff1ba0b86ce878405c59ff1956 Mon Sep 17 00:00:00 2001 From: misha-db Date: Tue, 23 Jun 2026 13:55:42 +0400 Subject: [PATCH 2/7] Handle python notebook creation --- .../workspace-fs/WorkspaceFsCommands.test.ts | 108 +++++++++++++++++- .../src/workspace-fs/WorkspaceFsCommands.ts | 29 ++++- 2 files changed, 134 insertions(+), 3 deletions(-) diff --git a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts index 90187e92b..130895016 100644 --- a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts +++ b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts @@ -1,5 +1,5 @@ import assert from "assert"; -import {EventEmitter, TreeView} from "vscode"; +import {EventEmitter, TreeView, Uri, window, workspace} from "vscode"; import {mock, instance, when} from "ts-mockito"; import {WorkspaceFsCommands} from "./WorkspaceFsCommands"; import {WorkspaceFsEntity} from "../sdk-extensions"; @@ -134,7 +134,7 @@ describe("WorkspaceFsCommands – target folder resolution", () => { }); it("no element → targets root", async () => { - await commands.createFile(undefined); + await commands.createFile(makeEntity(ROOT_PATH)); assert.strictEqual(capturedRootPath, ROOT_PATH); }); @@ -243,3 +243,107 @@ describe("WorkspaceFsCommands – target folder resolution", () => { }); }); }); + +describe("WorkspaceFsCommands – createFile content", () => { + const ROOT_PATH = "/Users/me"; + + let commands: WorkspaceFsCommands; + let capturedCreate: {path: string; content: string} | undefined; + + // Stubbed globals are restored after each test. + let originalShowInputBox: typeof window.showInputBox; + let originalShowTextDocument: typeof window.showTextDocument; + let originalOpenTextDocument: typeof workspace.openTextDocument; + let originalFromPath: typeof WorkspaceFsEntity.fromPath; + + let inputName: string; + + beforeEach(() => { + const mockConnectionManager = mock(); + when(mockConnectionManager.workspaceClient).thenReturn({} as any); + + // createDirWizard reads activeProjectUri to seed the input box. + const mockWorkspaceFolderManager = mock(); + when(mockWorkspaceFolderManager.activeProjectUri).thenReturn( + Uri.file("/tmp/project") + ); + + commands = new WorkspaceFsCommands( + instance(mockWorkspaceFolderManager), + instance(mockConnectionManager), + instance(mock()), + instance(mock()), + new FakeTreeView() as unknown as TreeView + ); + + // Fake root that captures what doCreateFile writes. + capturedCreate = undefined; + const fakeRoot = { + path: ROOT_PATH, + createFile: async (path: string, content: string) => { + capturedCreate = {path, content}; + }, + }; + (commands as any).getValidRoot = async () => fakeRoot; + + // createDirWizard reads the filename from showInputBox. + originalShowInputBox = window.showInputBox; + (window as any).showInputBox = async () => inputName; + + // No pre-existing file → skip the overwrite prompt. + originalFromPath = WorkspaceFsEntity.fromPath; + (WorkspaceFsEntity as any).fromPath = async () => undefined; + + // Avoid actually opening an editor after creation. + originalShowTextDocument = window.showTextDocument; + (window as any).showTextDocument = async () => undefined; + originalOpenTextDocument = workspace.openTextDocument; + (workspace as any).openTextDocument = async (uri: Uri) => uri; + }); + + afterEach(() => { + (window as any).showInputBox = originalShowInputBox; + (window as any).showTextDocument = originalShowTextDocument; + (workspace as any).openTextDocument = originalOpenTextDocument; + (WorkspaceFsEntity as any).fromPath = originalFromPath; + }); + + it(".ipynb file is created with valid empty notebook JSON", async () => { + inputName = "notebook.ipynb"; + await commands.createFile(makeEntity(ROOT_PATH)); + + assert.ok(capturedCreate, "createFile should have been called"); + assert.strictEqual(capturedCreate!.path, "notebook.ipynb"); + + const parsed = JSON.parse(capturedCreate!.content); + assert.deepStrictEqual(parsed.cells, []); + assert.strictEqual(parsed.nbformat, 4); + assert.strictEqual(parsed.nbformat_minor, 5); + assert.strictEqual(parsed.metadata.language_info.name, "python"); + }); + + it(".IPYNB extension is matched case-insensitively", async () => { + inputName = "NoteBook.IPYNB"; + await commands.createFile(makeEntity(ROOT_PATH)); + + assert.ok(capturedCreate); + const parsed = JSON.parse(capturedCreate!.content); + assert.strictEqual(parsed.nbformat, 4); + }); + + it(".py file is created with empty content", async () => { + inputName = "script.py"; + await commands.createFile(makeEntity(ROOT_PATH)); + + assert.ok(capturedCreate); + assert.strictEqual(capturedCreate!.content, ""); + }); + + it("plain file is created with empty content", async () => { + inputName = "notes.txt"; + await commands.createFile(makeEntity(ROOT_PATH)); + + assert.ok(capturedCreate); + assert.strictEqual(capturedCreate!.content, ""); + }); +}); diff --git a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts index b39766a6d..3ded2a5a5 100644 --- a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts +++ b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts @@ -16,6 +16,29 @@ import {WorkspaceFsFile} from "../sdk-extensions/wsfs/WorkspaceFsFile"; const withLogContext = logging.withLogContext; +/** + * Minimal valid empty Python notebook (nbformat 4.5) used as the initial + * content when creating a `.ipynb` file, so it can be opened as a notebook + * right away instead of as an invalid/empty document. + */ +const EMPTY_IPYNB_CONTENT = JSON.stringify( + { + cells: [], + metadata: { + kernelspec: { + display_name: "Python 3", + language: "python", + name: "python3", + }, + language_info: {name: "python"}, + }, + nbformat: 4, + nbformat_minor: 5, + }, + null, + 1 +); + export class WorkspaceFsCommands implements Disposable { private disposables: Disposable[] = []; @@ -202,8 +225,12 @@ export class WorkspaceFsCommands implements Disposable { } } + const content = inputName.toLowerCase().endsWith(".ipynb") + ? EMPTY_IPYNB_CONTENT + : ""; + try { - await root.createFile(inputName, "", true); + await root.createFile(inputName, content, true); } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); window.showErrorMessage(`Failed to create "${inputName}": ${msg}`); From 10be4b09b9dd76600c95dc5bdbba830318c3335f Mon Sep 17 00:00:00 2001 From: misha-db Date: Tue, 23 Jun 2026 14:35:52 +0400 Subject: [PATCH 3/7] handle opening ipynb notebook in browser --- .../workspace-fs/WorkspaceFsCommands.test.ts | 115 +++++++++++++++++- .../src/workspace-fs/WorkspaceFsCommands.ts | 30 +++-- 2 files changed, 134 insertions(+), 11 deletions(-) diff --git a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts index 130895016..bc6226b09 100644 --- a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts +++ b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts @@ -249,16 +249,32 @@ describe("WorkspaceFsCommands – createFile content", () => { let commands: WorkspaceFsCommands; let capturedCreate: {path: string; content: string} | undefined; + let lookedUpPaths: string[]; + let warningMessages: string[]; + let openedUri: Uri | undefined; + let browserOpenedPath: string | undefined; // Stubbed globals are restored after each test. let originalShowInputBox: typeof window.showInputBox; + let originalShowWarningMessage: typeof window.showWarningMessage; let originalShowTextDocument: typeof window.showTextDocument; let originalOpenTextDocument: typeof workspace.openTextDocument; let originalFromPath: typeof WorkspaceFsEntity.fromPath; let inputName: string; + // Path (relative to root) at which fromPath should report an existing + // entity, or undefined for "nothing exists". + let existingAt: string | undefined; + // Whether the user clicks "Overwrite" on the prompt. + let overwriteAnswer: string | undefined; beforeEach(() => { + existingAt = undefined; + overwriteAnswer = "Overwrite"; + lookedUpPaths = []; + warningMessages = []; + openedUri = undefined; + browserOpenedPath = undefined; const mockConnectionManager = mock(); when(mockConnectionManager.workspaceClient).thenReturn({} as any); @@ -276,33 +292,65 @@ describe("WorkspaceFsCommands – createFile content", () => { new FakeTreeView() as unknown as TreeView ); - // Fake root that captures what doCreateFile writes. + // Fake root that captures what doCreateFile writes. createFile mimics + // the SDK: a `.ipynb` is stored as a notebook at the stripped path. capturedCreate = undefined; const fakeRoot = { path: ROOT_PATH, createFile: async (path: string, content: string) => { capturedCreate = {path, content}; + const storedName = path.replace(/\.ipynb$/i, ""); + return { + path: `${ROOT_PATH}/${storedName}`, + } as unknown as WorkspaceFsEntity; }, }; (commands as any).getValidRoot = async () => fakeRoot; + // Capture browser opens (used for notebooks) instead of launching one. + (commands as any).openInBrowser = async ( + element: WorkspaceFsEntity + ) => { + browserOpenedPath = element.path; + }; + // createDirWizard reads the filename from showInputBox. originalShowInputBox = window.showInputBox; (window as any).showInputBox = async () => inputName; - // No pre-existing file → skip the overwrite prompt. + // fromPath reports an existing entity only at `existingAt`. originalFromPath = WorkspaceFsEntity.fromPath; - (WorkspaceFsEntity as any).fromPath = async () => undefined; + (WorkspaceFsEntity as any).fromPath = async ( + _client: unknown, + path: string + ) => { + lookedUpPaths.push(path); + return existingAt !== undefined && + path === `${ROOT_PATH}/${existingAt}` + ? ({path} as unknown as WorkspaceFsEntity) + : undefined; + }; + + // Capture the overwrite prompt and return the canned answer. + originalShowWarningMessage = window.showWarningMessage; + (window as any).showWarningMessage = async (message: string) => { + warningMessages.push(message); + return overwriteAnswer; + }; // Avoid actually opening an editor after creation. originalShowTextDocument = window.showTextDocument; (window as any).showTextDocument = async () => undefined; originalOpenTextDocument = workspace.openTextDocument; - (workspace as any).openTextDocument = async (uri: Uri) => uri; + (workspace as any).openTextDocument = async (uri: Uri) => { + openedUri = uri; + return uri; + }; }); afterEach(() => { (window as any).showInputBox = originalShowInputBox; + (window as any).showWarningMessage = originalShowWarningMessage; (window as any).showTextDocument = originalShowTextDocument; (workspace as any).openTextDocument = originalOpenTextDocument; (WorkspaceFsEntity as any).fromPath = originalFromPath; @@ -346,4 +394,63 @@ describe("WorkspaceFsCommands – createFile content", () => { assert.ok(capturedCreate); assert.strictEqual(capturedCreate!.content, ""); }); + + it(".ipynb existence check uses the extension-stripped path", async () => { + inputName = "notebook.ipynb"; + await commands.createFile(makeEntity(ROOT_PATH)); + + // The notebook clash is detected at the path without `.ipynb`. + assert.ok( + lookedUpPaths.includes(`${ROOT_PATH}/notebook`), + `expected lookup at stripped path, got ${JSON.stringify( + lookedUpPaths + )}` + ); + assert.ok(!lookedUpPaths.includes(`${ROOT_PATH}/notebook.ipynb`)); + }); + + it("prompts to overwrite an existing notebook with the same name", async () => { + inputName = "notebook.ipynb"; + existingAt = "notebook"; // notebook stored without extension + await commands.createFile(makeEntity(ROOT_PATH)); + + assert.strictEqual(warningMessages.length, 1); + assert.ok(warningMessages[0].includes('"notebook"')); + // User clicked Overwrite → file is still created. + assert.ok(capturedCreate); + }); + + it("aborts creation when overwrite is declined", async () => { + inputName = "notebook.ipynb"; + existingAt = "notebook"; + overwriteAnswer = undefined; // dismissed / not "Overwrite" + await commands.createFile(makeEntity(ROOT_PATH)); + + assert.strictEqual(warningMessages.length, 1); + assert.strictEqual( + capturedCreate, + undefined, + "createFile must not run when overwrite is declined" + ); + }); + + it("opens the created notebook in the browser at its stripped path", async () => { + inputName = "notebook.ipynb"; + await commands.createFile(makeEntity(ROOT_PATH)); + + assert.strictEqual(browserOpenedPath, `${ROOT_PATH}/notebook`); + // It must NOT also open in the text editor. + assert.strictEqual(openedUri, undefined); + }); + + it("non-notebook files are looked up and opened in the editor by exact name", async () => { + inputName = "script.py"; + await commands.createFile(makeEntity(ROOT_PATH)); + + assert.ok(lookedUpPaths.includes(`${ROOT_PATH}/script.py`)); + assert.ok(openedUri); + assert.strictEqual(openedUri!.path, `${ROOT_PATH}/script.py`); + // Non-notebooks must NOT open in the browser. + assert.strictEqual(browserOpenedPath, undefined); + }); }); diff --git a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts index 3ded2a5a5..8238ff73f 100644 --- a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts +++ b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts @@ -210,13 +210,18 @@ export class WorkspaceFsCommands implements Disposable { return; } + const isIpynb = inputName.toLowerCase().endsWith(".ipynb"); + const existingName = isIpynb + ? inputName.replace(/\.ipynb$/i, "") + : inputName; + const existing = await WorkspaceFsEntity.fromPath( client, - `${root.path}/${inputName}` + `${root.path}/${existingName}` ); if (existing) { const answer = await window.showWarningMessage( - `"${inputName}" already exists in the workspace. Overwrite it?`, + `"${existingName}" already exists in the workspace. Overwrite it?`, {modal: true}, "Overwrite" ); @@ -225,12 +230,11 @@ export class WorkspaceFsCommands implements Disposable { } } - const content = inputName.toLowerCase().endsWith(".ipynb") - ? EMPTY_IPYNB_CONTENT - : ""; + const content = isIpynb ? EMPTY_IPYNB_CONTENT : ""; + let created: WorkspaceFsEntity | undefined; try { - await root.createFile(inputName, content, true); + created = await root.createFile(inputName, content, true); } catch (e: unknown) { const msg = e instanceof Error ? e.message : String(e); window.showErrorMessage(`Failed to create "${inputName}": ${msg}`); @@ -240,10 +244,22 @@ export class WorkspaceFsCommands implements Disposable { this.workspaceFsDataProvider.refresh(); const uri = Uri.from({ scheme: WorkspaceFsFileSystemProvider.scheme, - path: `${root.path}/${inputName}`, + path: created?.path ?? `${root.path}/${inputName}`, }); this.fsp.notifyCreated(uri); + if (isIpynb && created) { + try { + await this.openInBrowser(created); + } catch (e: unknown) { + ctx?.logger?.error( + `Can't open ${inputName} in browser after creation`, + e + ); + } + return; + } + try { await window.showTextDocument( await workspace.openTextDocument(uri) From 8826fe8802256d7c213f2cd296683b5e35213940 Mon Sep 17 00:00:00 2001 From: misha-db Date: Tue, 23 Jun 2026 16:00:06 +0400 Subject: [PATCH 4/7] Linting fix --- .../databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts index 8238ff73f..552327d35 100644 --- a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts +++ b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts @@ -26,13 +26,16 @@ const EMPTY_IPYNB_CONTENT = JSON.stringify( cells: [], metadata: { kernelspec: { + // eslint-disable-next-line @typescript-eslint/naming-convention display_name: "Python 3", language: "python", name: "python3", }, + // eslint-disable-next-line @typescript-eslint/naming-convention language_info: {name: "python"}, }, nbformat: 4, + // eslint-disable-next-line @typescript-eslint/naming-convention nbformat_minor: 5, }, null, From 43978086d5b738a88d097b80c86d15620f3b918c Mon Sep 17 00:00:00 2001 From: misha-db Date: Wed, 24 Jun 2026 16:42:56 +0400 Subject: [PATCH 5/7] use WorkspaceFsUtils.isNotebook(created) to check if created file was notebook --- .../databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts index 552327d35..5f70715f1 100644 --- a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts +++ b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts @@ -251,7 +251,7 @@ export class WorkspaceFsCommands implements Disposable { }); this.fsp.notifyCreated(uri); - if (isIpynb && created) { + if (created && WorkspaceFsUtils.isNotebook(created)) { try { await this.openInBrowser(created); } catch (e: unknown) { From d181efda88fe623612583ad21203bf79685c4072 Mon Sep 17 00:00:00 2001 From: misha-db Date: Wed, 24 Jun 2026 16:56:17 +0400 Subject: [PATCH 6/7] Fix test --- .../src/workspace-fs/WorkspaceFsCommands.test.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts index bc6226b09..00a95c8ac 100644 --- a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts +++ b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.test.ts @@ -299,9 +299,11 @@ describe("WorkspaceFsCommands – createFile content", () => { path: ROOT_PATH, createFile: async (path: string, content: string) => { capturedCreate = {path, content}; + const isIpynb = /\.ipynb$/i.test(path); const storedName = path.replace(/\.ipynb$/i, ""); return { path: `${ROOT_PATH}/${storedName}`, + type: isIpynb ? "NOTEBOOK" : "FILE", } as unknown as WorkspaceFsEntity; }, }; From 8f88503c816d215739b5cef50b9c37e8312c9a32 Mon Sep 17 00:00:00 2001 From: misha-db Date: Wed, 24 Jun 2026 17:12:45 +0400 Subject: [PATCH 7/7] Make error message more specific --- .../src/workspace-fs/WorkspaceFsCommands.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts index 5f70715f1..18491ed91 100644 --- a/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts +++ b/packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts @@ -56,20 +56,21 @@ export class WorkspaceFsCommands implements Disposable { @withLogContext(Loggers.Extension) async getValidRoot( rootPath?: string, + itemType: "file" | "directory" = "directory", @context ctx?: Context ): Promise { if (!this.connectionManager.workspaceClient) { window.showErrorMessage( - `Please login first to create a new directory` + `Please login first to create a new ${itemType}` ); return; } if (!rootPath) { ctx?.logger?.error( - "No root path when trying to create a directory" + `No root path when trying to create a ${itemType}` ); - window.showErrorMessage("Error when creating a new directory"); + window.showErrorMessage(`Error when creating a new ${itemType}`); return; } @@ -86,10 +87,10 @@ export class WorkspaceFsCommands implements Disposable { if (!WorkspaceFsUtils.isDirectory(root)) { ctx?.logger?.error( - `Cannot create a directory as a child of a ${root?.type}` + `Cannot create a ${itemType} as a child of a ${root?.type}` ); window.showErrorMessage( - `Cannot create a directory as a child of a ${root?.type}` + `Cannot create a ${itemType} as a child of a ${root?.type}` ); return; } @@ -131,7 +132,7 @@ export class WorkspaceFsCommands implements Disposable { rootPath: string | undefined, @context ctx?: Context ) { - const root = await this.getValidRoot(rootPath, ctx); + const root = await this.getValidRoot(rootPath, "directory", ctx); if (!root) { return; } @@ -198,7 +199,7 @@ export class WorkspaceFsCommands implements Disposable { return; } - const root = await this.getValidRoot(rootPath, ctx); + const root = await this.getValidRoot(rootPath, "file", ctx); if (!root) { return; } @@ -359,7 +360,7 @@ export class WorkspaceFsCommands implements Disposable { return; } - const root = await this.getValidRoot(rootPath); + const root = await this.getValidRoot(rootPath, "file"); if (!root) { return; }