Skip to content

Commit 7a82677

Browse files
committed
Add FileSystemProvider, New File command, and workspace folder mount
- Add WebDavFileSystemProvider with caching, stat/readDirectory/readFile/ writeFile/createDirectory/delete operations against WebDAV - Root path handling returns synthetic directory listing of the 9 well-known B2C Commerce roots (avoids PROPFIND on "/") - Tree provider delegates to FS provider instead of calling WebDAV directly; root nodes use standard folder icons via resourceUri - New File command: prompts for filename, creates empty file, opens in editor - Mount/Unmount Workspace commands: add/remove b2c-webdav:/ as a VS Code workspace folder for native Explorer integration - Context key b2c-dx.webdav.mounted tracks mount state for menu visibility - Download command available in native Explorer context menu for b2c-webdav files
1 parent 7418a9f commit 7a82677

File tree

5 files changed

+444
-118
lines changed

5 files changed

+444
-118
lines changed

packages/b2c-vs-extension/package.json

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
},
2020
"activationEvents": [
2121
"onView:b2cWebdavExplorer",
22+
"onFileSystem:b2c-webdav",
2223
"onCommand:b2c-dx.openUI",
2324
"onCommand:b2c-dx.handleStorefrontNextCartridge",
2425
"onCommand:b2c-dx.promptAgent",
@@ -119,6 +120,24 @@
119120
"title": "Open File",
120121
"icon": "$(go-to-file)",
121122
"category": "B2C DX"
123+
},
124+
{
125+
"command": "b2c-dx.webdav.newFile",
126+
"title": "New File",
127+
"icon": "$(new-file)",
128+
"category": "B2C DX"
129+
},
130+
{
131+
"command": "b2c-dx.webdav.mountWorkspace",
132+
"title": "Open as Workspace Folder",
133+
"icon": "$(root-folder-opened)",
134+
"category": "B2C DX"
135+
},
136+
{
137+
"command": "b2c-dx.webdav.unmountWorkspace",
138+
"title": "Remove Workspace Folder",
139+
"icon": "$(root-folder)",
140+
"category": "B2C DX"
122141
}
123142
],
124143
"menus": {
@@ -127,9 +146,24 @@
127146
"command": "b2c-dx.webdav.refresh",
128147
"when": "view == b2cWebdavExplorer",
129148
"group": "navigation"
149+
},
150+
{
151+
"command": "b2c-dx.webdav.mountWorkspace",
152+
"when": "view == b2cWebdavExplorer && !b2c-dx.webdav.mounted",
153+
"group": "navigation"
154+
},
155+
{
156+
"command": "b2c-dx.webdav.unmountWorkspace",
157+
"when": "view == b2cWebdavExplorer && b2c-dx.webdav.mounted",
158+
"group": "navigation"
130159
}
131160
],
132161
"view/item/context": [
162+
{
163+
"command": "b2c-dx.webdav.newFile",
164+
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|directory)$/",
165+
"group": "1_modification@0"
166+
},
133167
{
134168
"command": "b2c-dx.webdav.newFolder",
135169
"when": "view == b2cWebdavExplorer && viewItem =~ /^(root|directory)$/",
@@ -155,6 +189,13 @@
155189
"when": "view == b2cWebdavExplorer && viewItem =~ /^(directory|file)$/",
156190
"group": "2_destructive@1"
157191
}
192+
],
193+
"explorer/context": [
194+
{
195+
"command": "b2c-dx.webdav.download",
196+
"when": "resourceScheme == b2c-webdav && !explorerResourceIsFolder",
197+
"group": "navigation"
198+
}
158199
]
159200
}
160201
},

packages/b2c-vs-extension/src/webdav-tree/index.ts

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,35 @@
55
*/
66
import * as vscode from 'vscode';
77
import {WebDavConfigProvider} from './webdav-config.js';
8+
import {WEBDAV_SCHEME, WebDavFileSystemProvider} from './webdav-fs-provider.js';
89
import {WebDavTreeDataProvider} from './webdav-tree-provider.js';
910
import {registerWebDavCommands} from './webdav-commands.js';
1011

12+
function syncMountedContext(): void {
13+
const mounted = (vscode.workspace.workspaceFolders ?? []).some((f) => f.uri.scheme === WEBDAV_SCHEME);
14+
vscode.commands.executeCommand('setContext', 'b2c-dx.webdav.mounted', mounted);
15+
}
16+
1117
export function registerWebDavTree(context: vscode.ExtensionContext): void {
1218
const configProvider = new WebDavConfigProvider();
13-
const treeProvider = new WebDavTreeDataProvider(configProvider);
19+
const fsProvider = new WebDavFileSystemProvider(configProvider);
20+
21+
const fsRegistration = vscode.workspace.registerFileSystemProvider(WEBDAV_SCHEME, fsProvider, {
22+
isCaseSensitive: true,
23+
});
24+
25+
const treeProvider = new WebDavTreeDataProvider(configProvider, fsProvider);
1426

1527
const treeView = vscode.window.createTreeView('b2cWebdavExplorer', {
1628
treeDataProvider: treeProvider,
1729
showCollapseAll: true,
1830
});
1931

20-
const commandDisposables = registerWebDavCommands(context, configProvider, treeProvider);
32+
const commandDisposables = registerWebDavCommands(context, configProvider, treeProvider, fsProvider);
33+
34+
// Sync the mounted context key on activation and when workspace folders change
35+
syncMountedContext();
36+
const folderWatcher = vscode.workspace.onDidChangeWorkspaceFolders(() => syncMountedContext());
2137

22-
context.subscriptions.push(treeView, ...commandDisposables);
38+
context.subscriptions.push(fsRegistration, treeView, folderWatcher, ...commandDisposables);
2339
}

packages/b2c-vs-extension/src/webdav-tree/webdav-commands.ts

Lines changed: 52 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,25 +7,23 @@ import * as fs from 'fs';
77
import * as path from 'path';
88
import * as vscode from 'vscode';
99
import type {WebDavConfigProvider} from './webdav-config.js';
10+
import {type WebDavFileSystemProvider, WEBDAV_SCHEME, webdavPathToUri} from './webdav-fs-provider.js';
1011
import type {WebDavTreeDataProvider, WebDavTreeItem} from './webdav-tree-provider.js';
1112

1213
export function registerWebDavCommands(
13-
context: vscode.ExtensionContext,
14+
_context: vscode.ExtensionContext,
1415
configProvider: WebDavConfigProvider,
1516
treeProvider: WebDavTreeDataProvider,
17+
fsProvider: WebDavFileSystemProvider,
1618
): vscode.Disposable[] {
1719
const refresh = vscode.commands.registerCommand('b2c-dx.webdav.refresh', () => {
20+
fsProvider.clearCache();
1821
configProvider.reset();
1922
treeProvider.refresh();
2023
});
2124

2225
const newFolder = vscode.commands.registerCommand('b2c-dx.webdav.newFolder', async (node: WebDavTreeItem) => {
2326
if (!node) return;
24-
const instance = configProvider.getInstance();
25-
if (!instance) {
26-
vscode.window.showErrorMessage('WebDAV: No B2C instance configured.');
27-
return;
28-
}
2927

3028
const name = await vscode.window.showInputBox({
3129
title: 'New Folder',
@@ -45,8 +43,7 @@ export function registerWebDavCommands(
4543
{location: vscode.ProgressLocation.Notification, title: `Creating folder ${name.trim()}...`},
4644
async () => {
4745
try {
48-
await instance.webdav.mkcol(fullPath);
49-
treeProvider.refreshNode(node);
46+
await fsProvider.createDirectory(webdavPathToUri(fullPath));
5047
} catch (err) {
5148
const message = err instanceof Error ? err.message : String(err);
5249
vscode.window.showErrorMessage(`WebDAV: Failed to create folder: ${message}`);
@@ -57,11 +54,6 @@ export function registerWebDavCommands(
5754

5855
const uploadFile = vscode.commands.registerCommand('b2c-dx.webdav.uploadFile', async (node: WebDavTreeItem) => {
5956
if (!node) return;
60-
const instance = configProvider.getInstance();
61-
if (!instance) {
62-
vscode.window.showErrorMessage('WebDAV: No B2C instance configured.');
63-
return;
64-
}
6557

6658
const uris = await vscode.window.showOpenDialog({
6759
title: 'Select file to upload',
@@ -80,19 +72,10 @@ export function registerWebDavCommands(
8072
async () => {
8173
try {
8274
const content = fs.readFileSync(uri.fsPath);
83-
const ext = path.extname(fileName).toLowerCase();
84-
const mime: Record<string, string> = {
85-
'.json': 'application/json',
86-
'.xml': 'application/xml',
87-
'.zip': 'application/zip',
88-
'.js': 'application/javascript',
89-
'.ts': 'application/typescript',
90-
'.html': 'text/html',
91-
'.css': 'text/css',
92-
'.txt': 'text/plain',
93-
};
94-
await instance.webdav.put(fullPath, content, mime[ext]);
95-
treeProvider.refreshNode(node);
75+
await fsProvider.writeFile(webdavPathToUri(fullPath), new Uint8Array(content), {
76+
create: true,
77+
overwrite: true,
78+
});
9679
} catch (err) {
9780
const message = err instanceof Error ? err.message : String(err);
9881
vscode.window.showErrorMessage(`WebDAV: Upload failed: ${message}`);
@@ -103,11 +86,6 @@ export function registerWebDavCommands(
10386

10487
const deleteItem = vscode.commands.registerCommand('b2c-dx.webdav.delete', async (node: WebDavTreeItem) => {
10588
if (!node) return;
106-
const instance = configProvider.getInstance();
107-
if (!instance) {
108-
vscode.window.showErrorMessage('WebDAV: No B2C instance configured.');
109-
return;
110-
}
11189

11290
const detail = node.isCollection
11391
? 'This directory and its contents will be deleted.'
@@ -124,10 +102,7 @@ export function registerWebDavCommands(
124102
{location: vscode.ProgressLocation.Notification, title: `Deleting ${node.fileName}...`},
125103
async () => {
126104
try {
127-
await instance.webdav.delete(node.webdavPath);
128-
// Refresh parent by refreshing the whole tree — the parent node
129-
// is not directly available from the child.
130-
treeProvider.refresh();
105+
await fsProvider.delete(webdavPathToUri(node.webdavPath));
131106
} catch (err) {
132107
const message = err instanceof Error ? err.message : String(err);
133108
vscode.window.showErrorMessage(`WebDAV: Delete failed: ${message}`);
@@ -138,11 +113,6 @@ export function registerWebDavCommands(
138113

139114
const download = vscode.commands.registerCommand('b2c-dx.webdav.download', async (node: WebDavTreeItem) => {
140115
if (!node) return;
141-
const instance = configProvider.getInstance();
142-
if (!instance) {
143-
vscode.window.showErrorMessage('WebDAV: No B2C instance configured.');
144-
return;
145-
}
146116

147117
const defaultUri = vscode.workspace.workspaceFolders?.[0]?.uri
148118
? vscode.Uri.joinPath(vscode.workspace.workspaceFolders[0].uri, node.fileName)
@@ -157,8 +127,8 @@ export function registerWebDavCommands(
157127
{location: vscode.ProgressLocation.Notification, title: `Downloading ${node.fileName}...`},
158128
async () => {
159129
try {
160-
const buffer = await instance.webdav.get(node.webdavPath);
161-
await vscode.workspace.fs.writeFile(saveUri, new Uint8Array(buffer));
130+
const content = await fsProvider.readFile(webdavPathToUri(node.webdavPath));
131+
await vscode.workspace.fs.writeFile(saveUri, content);
162132
vscode.window.showInformationMessage(`Downloaded to ${saveUri.fsPath}`);
163133
} catch (err) {
164134
const message = err instanceof Error ? err.message : String(err);
@@ -170,32 +140,56 @@ export function registerWebDavCommands(
170140

171141
const openFile = vscode.commands.registerCommand('b2c-dx.webdav.openFile', async (node: WebDavTreeItem) => {
172142
if (!node) return;
173-
const instance = configProvider.getInstance();
174-
if (!instance) {
175-
vscode.window.showErrorMessage('WebDAV: No B2C instance configured.');
176-
return;
177-
}
143+
const uri = webdavPathToUri(node.webdavPath);
144+
await vscode.commands.executeCommand('vscode.open', uri);
145+
});
178146

179-
const previewDir = vscode.Uri.joinPath(context.globalStorageUri, 'webdav-preview');
180-
const tempFileUri = vscode.Uri.joinPath(previewDir, node.webdavPath);
147+
const newFile = vscode.commands.registerCommand('b2c-dx.webdav.newFile', async (node: WebDavTreeItem) => {
148+
if (!node) return;
181149

150+
const name = await vscode.window.showInputBox({
151+
title: 'New File',
152+
prompt: `Create file under ${node.webdavPath}`,
153+
placeHolder: 'File name',
154+
validateInput: (value: string) => {
155+
const trimmed = value.trim();
156+
if (!trimmed) return 'Enter a file name';
157+
if (/[\\/:*?"<>|]/.test(trimmed)) return 'Name cannot contain \\ / : * ? " < > |';
158+
return null;
159+
},
160+
});
161+
if (!name) return;
162+
163+
const fullPath = `${node.webdavPath}/${name.trim()}`;
164+
const uri = webdavPathToUri(fullPath);
182165
await vscode.window.withProgress(
183-
{location: vscode.ProgressLocation.Notification, title: `Opening ${node.fileName}...`},
166+
{location: vscode.ProgressLocation.Notification, title: `Creating file ${name.trim()}...`},
184167
async () => {
185168
try {
186-
const buffer = await instance.webdav.get(node.webdavPath);
187-
// Ensure parent directories exist
188-
const parentDir = vscode.Uri.joinPath(tempFileUri, '..');
189-
await vscode.workspace.fs.createDirectory(parentDir);
190-
await vscode.workspace.fs.writeFile(tempFileUri, new Uint8Array(buffer));
191-
await vscode.commands.executeCommand('vscode.open', tempFileUri);
169+
await fsProvider.writeFile(uri, new Uint8Array(0), {create: true, overwrite: false});
170+
await vscode.commands.executeCommand('vscode.open', uri);
192171
} catch (err) {
193172
const message = err instanceof Error ? err.message : String(err);
194-
vscode.window.showErrorMessage(`WebDAV: Failed to open file: ${message}`);
173+
vscode.window.showErrorMessage(`WebDAV: Failed to create file: ${message}`);
195174
}
196175
},
197176
);
198177
});
199178

200-
return [refresh, newFolder, uploadFile, deleteItem, download, openFile];
179+
const mountWorkspace = vscode.commands.registerCommand('b2c-dx.webdav.mountWorkspace', () => {
180+
vscode.workspace.updateWorkspaceFolders(vscode.workspace.workspaceFolders?.length ?? 0, 0, {
181+
uri: vscode.Uri.parse(`${WEBDAV_SCHEME}:/`),
182+
name: 'B2C Commerce WebDAV',
183+
});
184+
});
185+
186+
const unmountWorkspace = vscode.commands.registerCommand('b2c-dx.webdav.unmountWorkspace', () => {
187+
const folders = vscode.workspace.workspaceFolders ?? [];
188+
const idx = folders.findIndex((f) => f.uri.scheme === WEBDAV_SCHEME);
189+
if (idx >= 0) {
190+
vscode.workspace.updateWorkspaceFolders(idx, 1);
191+
}
192+
});
193+
194+
return [refresh, newFolder, newFile, uploadFile, deleteItem, download, openFile, mountWorkspace, unmountWorkspace];
201195
}

0 commit comments

Comments
 (0)