Skip to content
Open
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
11 changes: 6 additions & 5 deletions packages/databricks-vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -393,11 +393,15 @@ export async function activate(
connectionManager
);
const workspaceFsFsp = new WorkspaceFsFileSystemProvider(connectionManager);
const workspaceFsTreeView = window.createTreeView("workspaceFsView", {
treeDataProvider: workspaceFsDataProvider,
});
const workspaceFsCommands = new WorkspaceFsCommands(
workspaceFolderManager,
connectionManager,
workspaceFsDataProvider,
workspaceFsFsp
workspaceFsFsp,
workspaceFsTreeView
);

context.subscriptions.push(
Expand All @@ -409,10 +413,7 @@ export async function activate(
}
),
workspaceFsFsp,
window.registerTreeDataProvider(
"workspaceFsView",
workspaceFsDataProvider
),
workspaceFsTreeView,
telemetry.registerCommand(
"databricks.wsfs.refresh",
workspaceFsCommands.refresh,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
import assert from "assert";
import {EventEmitter, TreeView} from "vscode";
import {mock, instance, when} from "ts-mockito";
import {WorkspaceFsCommands} from "./WorkspaceFsCommands";
import {WorkspaceFsEntity} from "../sdk-extensions";
import {ConnectionManager} from "../configuration/ConnectionManager";
import {WorkspaceFsDataProvider} from "./WorkspaceFsDataProvider";
import {WorkspaceFsFileSystemProvider} from "./WorkspaceFsFileSystemProvider";
import {WorkspaceFolderManager} from "../vscode-objs/WorkspaceFolderManager";
import {DatabricksWorkspace} from "../configuration/DatabricksWorkspace";
import {RemoteUri} from "../sync/SyncDestination";

// Minimal fake TreeView that tracks selection and fires onDidChangeSelection
class FakeTreeView {
selection: ReadonlyArray<WorkspaceFsEntity> = [];
private _emitter = new EventEmitter<{
selection: WorkspaceFsEntity[];
}>();
readonly onDidChangeSelection = this._emitter.event;

simulateSelect(element: WorkspaceFsEntity | undefined): void {
this.selection = element ? [element] : [];
this._emitter.fire({selection: [...this.selection]});
}
}

function makeEntity(path: string): WorkspaceFsEntity {
return {path, type: "DIRECTORY"} as unknown as WorkspaceFsEntity;
}

describe("WorkspaceFsCommands – target folder resolution", () => {
const ROOT_PATH = "/Users/me";

let fakeTreeView: FakeTreeView;
let mockConnectionManager: ConnectionManager;
let commands: WorkspaceFsCommands;
let capturedRootPath: string | undefined;

const entityA = makeEntity("/Users/me/A");
const entityB = makeEntity("/Users/me/B");

beforeEach(() => {
fakeTreeView = new FakeTreeView();

const mockDatabricksWorkspace = mock<DatabricksWorkspace>();
when(mockDatabricksWorkspace.currentFsRoot).thenReturn(
new RemoteUri(ROOT_PATH)
);

mockConnectionManager = mock<ConnectionManager>();
when(mockConnectionManager.workspaceClient).thenReturn(
undefined as any
);
when(mockConnectionManager.databricksWorkspace).thenReturn(
instance(mockDatabricksWorkspace)
);

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

// Replace getValidRoot to capture rootPath and short-circuit execution
capturedRootPath = undefined;
(commands as any).getValidRoot = async (rootPath?: string) => {
capturedRootPath = rootPath;
return undefined;
};
});

describe("createFolder", () => {
it("title bar, nothing selected → targets root", async () => {
await commands.createFolder(undefined);
assert.strictEqual(capturedRootPath, ROOT_PATH);
});

it("title bar, A selected → targets A", async () => {
fakeTreeView.simulateSelect(entityA);
await commands.createFolder(entityA);
assert.strictEqual(capturedRootPath, entityA.path);
});

it("title bar, A selected then deselected → targets root", async () => {
fakeTreeView.simulateSelect(entityA);
fakeTreeView.simulateSelect(undefined);
await commands.createFolder(undefined);
assert.strictEqual(capturedRootPath, ROOT_PATH);
});

it("context menu on B while A is selected → targets B", async () => {
fakeTreeView.simulateSelect(entityA);
// Right-click on B does NOT update selection; selection[0] is still A
await commands.createFolder(entityB);
assert.strictEqual(capturedRootPath, entityB.path);
});

it("context menu on B with nothing selected → targets B", async () => {
await commands.createFolder(entityB);
assert.strictEqual(capturedRootPath, entityB.path);
});

it("context menu on A while A is selected → targets A", async () => {
fakeTreeView.simulateSelect(entityA);
// Right-click on the already-selected item: element === selection[0]
await commands.createFolder(entityA);
assert.strictEqual(capturedRootPath, entityA.path);
});
});

describe("uploadFile", () => {
// uploadFile bails early when workspaceClient is undefined, before
// reaching getValidRoot. Provide a non-null client so the activeElement
// logic is exercised.
beforeEach(() => {
when(mockConnectionManager.workspaceClient).thenReturn({} as any);
});

it("title bar, nothing selected → targets root", async () => {
await commands.uploadFile(undefined);
assert.strictEqual(capturedRootPath, ROOT_PATH);
});

it("title bar, A selected → targets A", async () => {
fakeTreeView.simulateSelect(entityA);
await commands.uploadFile(entityA);
assert.strictEqual(capturedRootPath, entityA.path);
});

it("title bar, A selected then deselected → targets root", async () => {
fakeTreeView.simulateSelect(entityA);
fakeTreeView.simulateSelect(undefined);
await commands.uploadFile(undefined);
assert.strictEqual(capturedRootPath, ROOT_PATH);
});

it("context menu on B while A is selected → targets B", async () => {
fakeTreeView.simulateSelect(entityA);
await commands.uploadFile(entityB);
assert.strictEqual(capturedRootPath, entityB.path);
});

it("context menu on B with nothing selected → targets B", async () => {
await commands.uploadFile(entityB);
assert.strictEqual(capturedRootPath, entityB.path);
});

it("context menu on A while A is selected → targets A", async () => {
fakeTreeView.simulateSelect(entityA);
await commands.uploadFile(entityA);
assert.strictEqual(capturedRootPath, entityA.path);
});

it("title bar, file (non-directory) selected → targets root", async () => {
const fileEntity = {
path: "/Users/me/A/note.py",
type: "FILE",
} as unknown as WorkspaceFsEntity;
fakeTreeView.simulateSelect(fileEntity);
await commands.uploadFile(fileEntity);
assert.strictEqual(capturedRootPath, ROOT_PATH);
});
});
});
31 changes: 25 additions & 6 deletions packages/databricks-vscode/src/workspace-fs/WorkspaceFsCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
WorkspaceFsUtils,
} from "../sdk-extensions";
import {context, Context} from "@databricks/sdk-experimental/dist/context";
import {Disposable, Uri, env, window, workspace} from "vscode";
import {Disposable, TreeView, Uri, env, window, workspace} from "vscode";
import {ConnectionManager} from "../configuration/ConnectionManager";
import {Loggers} from "../logger";
import {createDirWizard} from "./createDirectoryWizard";
Expand All @@ -18,13 +18,21 @@ const withLogContext = logging.withLogContext;

export class WorkspaceFsCommands implements Disposable {
private disposables: Disposable[] = [];
private selectedElement: WorkspaceFsEntity | undefined;

constructor(
private workspaceFolderManager: WorkspaceFolderManager,
private connectionManager: ConnectionManager,
private workspaceFsDataProvider: WorkspaceFsDataProvider,
private fsp: WorkspaceFsFileSystemProvider
) {}
private fsp: WorkspaceFsFileSystemProvider,
private readonly treeView: TreeView<WorkspaceFsEntity>
) {
this.disposables.push(
treeView.onDidChangeSelection((e) => {
this.selectedElement = e.selection[0];
})
);
}

@withLogContext(Loggers.Extension)
async getValidRoot(
Expand Down Expand Up @@ -70,10 +78,19 @@ export class WorkspaceFsCommands implements Disposable {
return root;
}

private resolveTargetElement(
element?: WorkspaceFsEntity
): WorkspaceFsEntity | undefined {
return element !== this.treeView.selection[0]
? element
: this.selectedElement;
}

@withLogContext(Loggers.Extension)
async createFolder(element?: WorkspaceFsEntity, @context ctx?: Context) {
const activeElement = this.resolveTargetElement(element);
const rootPath =
element?.path ??
activeElement?.path ??
this.connectionManager.databricksWorkspace?.currentFsRoot.path;

const root = await this.getValidRoot(rootPath, ctx);
Expand Down Expand Up @@ -179,9 +196,11 @@ export class WorkspaceFsCommands implements Disposable {
return;
}

const activeElement = this.resolveTargetElement(element);
const rootPath =
(element?.type === "DIRECTORY" || element?.type === "REPO"
? element?.path
(activeElement?.type === "DIRECTORY" ||
activeElement?.type === "REPO"
? activeElement?.path
: undefined) ??
this.connectionManager.databricksWorkspace?.currentFsRoot.path;

Expand Down
Loading