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
26 changes: 25 additions & 1 deletion packages/databricks-vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -726,6 +740,11 @@
"when": "view == workspaceFsView",
"group": "navigation@1"
},
{
"command": "databricks.wsfs.createNewFile.toolbar",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (optional): this toolbar button shares navigation@1 with createFolder.toolbar and uploadFile.toolbar, so ordering between the three is implicit. The context-menu entries use explicit wsfs_mut@0/@1/@2; consider the same explicit ordering here for predictability. Pre-existing pattern, so optional.

Comment thread
rugpanov marked this conversation as resolved.
"when": "view == workspaceFsView",
"group": "navigation@1"
},
{
"command": "databricks.unityCatalog.filter",
"when": "view == unityCatalogView",
Expand Down Expand Up @@ -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)",
Expand Down
10 changes: 10 additions & 0 deletions packages/databricks-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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(makeEntity(ROOT_PATH));
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.
Expand Down Expand Up @@ -190,3 +243,216 @@ describe("WorkspaceFsCommands – target folder resolution", () => {
});
});
});

describe("WorkspaceFsCommands – createFile content", () => {
const ROOT_PATH = "/Users/me";

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<ConnectionManager>();
when(mockConnectionManager.workspaceClient).thenReturn({} as any);

// createDirWizard reads activeProjectUri to seed the input box.
const mockWorkspaceFolderManager = mock<WorkspaceFolderManager>();
when(mockWorkspaceFolderManager.activeProjectUri).thenReturn(
Uri.file("/tmp/project")
);

commands = new WorkspaceFsCommands(
instance(mockWorkspaceFolderManager),
instance(mockConnectionManager),
instance(mock<WorkspaceFsDataProvider>()),
instance(mock<WorkspaceFsFileSystemProvider>()),
new FakeTreeView() as unknown as TreeView<WorkspaceFsEntity>
);

// 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 isIpynb = /\.ipynb$/i.test(path);
const storedName = path.replace(/\.ipynb$/i, "");
return {
path: `${ROOT_PATH}/${storedName}`,
type: isIpynb ? "NOTEBOOK" : "FILE",
} 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;

// fromPath reports an existing entity only at `existingAt`.
originalFromPath = WorkspaceFsEntity.fromPath;
(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) => {
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;
});

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, "");
});

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);
});
});
Loading
Loading