fix(vscode): add project adds the chosen folder to the workspace
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import { beforeEach, describe, expect, mock, test } from 'bun:test';
|
||||
|
||||
const executeCommand = mock(async () => undefined);
|
||||
const updateWorkspaceFolders = mock(async (start, deleteCount, ...foldersToAdd) => {
|
||||
for (const folder of foldersToAdd) {
|
||||
currentWorkspaceFolders = [...currentWorkspaceFolders, { name: folder.uri.fsPath.split('/').pop(), uri: folder.uri }];
|
||||
}
|
||||
return true;
|
||||
});
|
||||
let currentWorkspaceFolders = [];
|
||||
|
||||
class Position {
|
||||
constructor(line, character) {
|
||||
@@ -19,7 +26,10 @@ class Range {
|
||||
mock.module('vscode', () => ({
|
||||
commands: { executeCommand },
|
||||
workspace: {
|
||||
workspaceFolders: [],
|
||||
get workspaceFolders() {
|
||||
return currentWorkspaceFolders;
|
||||
},
|
||||
updateWorkspaceFolders,
|
||||
},
|
||||
Uri: {
|
||||
file: (fsPath) => ({ scheme: 'file', fsPath }),
|
||||
@@ -65,6 +75,8 @@ const deps = {
|
||||
describe('VS Code system bridge editor:openFile', () => {
|
||||
beforeEach(() => {
|
||||
executeCommand.mockClear();
|
||||
updateWorkspaceFolders.mockClear();
|
||||
currentWorkspaceFolders = [];
|
||||
});
|
||||
|
||||
test('uses vscode.open so VS Code can select the notebook editor', async () => {
|
||||
@@ -97,3 +109,113 @@ describe('VS Code system bridge editor:openFile', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('VS Code system bridge api:workspace:addFolder', () => {
|
||||
beforeEach(() => {
|
||||
updateWorkspaceFolders.mockClear();
|
||||
currentWorkspaceFolders = [];
|
||||
});
|
||||
|
||||
test('adds a folder to the workspace and returns the folder list', async () => {
|
||||
currentWorkspaceFolders = [{ name: 'project-one', uri: { fsPath: '/workspace/project-one' } }];
|
||||
|
||||
const response = await handleSystemBridgeMessage({
|
||||
id: 'add-folder',
|
||||
type: 'api:workspace:addFolder',
|
||||
payload: { path: '/home/user/my-project' },
|
||||
}, undefined, deps);
|
||||
|
||||
expect(response).toEqual({
|
||||
id: 'add-folder',
|
||||
type: 'api:workspace:addFolder',
|
||||
success: true,
|
||||
data: {
|
||||
workspaceFolders: [
|
||||
{ name: 'my-project', path: '/home/user/my-project' },
|
||||
{ name: 'project-one', path: '/workspace/project-one' },
|
||||
],
|
||||
},
|
||||
});
|
||||
expect(updateWorkspaceFolders).toHaveBeenCalledWith(
|
||||
1,
|
||||
null,
|
||||
{ uri: { scheme: 'file', fsPath: '/home/user/my-project' } },
|
||||
);
|
||||
});
|
||||
|
||||
test('does not duplicate an already-open workspace folder', async () => {
|
||||
currentWorkspaceFolders = [{ name: 'project-one', uri: { fsPath: '/workspace/project-one' } }];
|
||||
|
||||
const response = await handleSystemBridgeMessage({
|
||||
id: 'add-existing',
|
||||
type: 'api:workspace:addFolder',
|
||||
payload: { path: '/workspace/project-one' },
|
||||
}, undefined, deps);
|
||||
|
||||
expect(response).toEqual({
|
||||
id: 'add-existing',
|
||||
type: 'api:workspace:addFolder',
|
||||
success: true,
|
||||
data: {
|
||||
workspaceFolders: [{ name: 'project-one', path: '/workspace/project-one' }],
|
||||
},
|
||||
});
|
||||
expect(updateWorkspaceFolders).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('returns an error when VS Code rejects the folder add', async () => {
|
||||
updateWorkspaceFolders.mockResolvedValue(false);
|
||||
|
||||
const response = await handleSystemBridgeMessage({
|
||||
id: 'add-rejected',
|
||||
type: 'api:workspace:addFolder',
|
||||
payload: { path: '/home/user/other' },
|
||||
}, undefined, deps);
|
||||
|
||||
expect(response).toEqual({
|
||||
id: 'add-rejected',
|
||||
type: 'api:workspace:addFolder',
|
||||
success: false,
|
||||
error: 'Failed to add workspace folder',
|
||||
});
|
||||
});
|
||||
|
||||
test('dedupes an already-open folder with a lowercase Windows drive letter', async () => {
|
||||
// VS Code reports workspace folder paths with lowercase drive letters
|
||||
// (d:\...), while the bridge normalizes the incoming path to uppercase
|
||||
// (D:\...). The comparison must normalize both sides.
|
||||
currentWorkspaceFolders = [{ name: 'project-one', uri: { fsPath: 'd:\\work\\project-one' } }];
|
||||
|
||||
const response = await handleSystemBridgeMessage({
|
||||
id: 'add-win-dedupe',
|
||||
type: 'api:workspace:addFolder',
|
||||
payload: { path: 'D:\\work\\project-one' },
|
||||
}, undefined, deps);
|
||||
|
||||
expect(response).toEqual({
|
||||
id: 'add-win-dedupe',
|
||||
type: 'api:workspace:addFolder',
|
||||
success: true,
|
||||
data: {
|
||||
workspaceFolders: [{ name: 'project-one', path: 'D:\\work\\project-one' }],
|
||||
},
|
||||
});
|
||||
expect(updateWorkspaceFolders).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
test('rejects a missing path', async () => {
|
||||
const response = await handleSystemBridgeMessage({
|
||||
id: 'add-missing',
|
||||
type: 'api:workspace:addFolder',
|
||||
payload: {},
|
||||
}, undefined, deps);
|
||||
|
||||
expect(response).toEqual({
|
||||
id: 'add-missing',
|
||||
type: 'api:workspace:addFolder',
|
||||
success: false,
|
||||
error: 'Directory path is required',
|
||||
});
|
||||
expect(updateWorkspaceFolders).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -10,6 +10,8 @@ import { credentialStatus, deleteCredential, importCursorCredential, normalizeCr
|
||||
import { getSessionActivitySnapshot } from './sessionActivityWatcher';
|
||||
import { getOpenCodeUpgradeStatus, upgradeManagedOpenCode } from './opencode-upgrade-runtime';
|
||||
import { buildDeferredRestartResponse } from './config-mutation-response';
|
||||
import { normalizeWindowsDriveLetter } from './pathUtils';
|
||||
import { resolveWorkspaceFolders } from './workspaceResolver';
|
||||
import type { BridgeContext, BridgeResponse } from './bridge';
|
||||
|
||||
type BridgeMessageInput = {
|
||||
@@ -594,6 +596,41 @@ export async function handleSystemBridgeMessage(
|
||||
}
|
||||
}
|
||||
|
||||
case 'api:workspace:addFolder': {
|
||||
try {
|
||||
// SAFETY: bridge payloads are untrusted JSON from the webview; the
|
||||
// cast only reads the optional path field, and non-string values fail
|
||||
// the emptiness check below (or throw inside the try, which the catch
|
||||
// converts into a clean failure response).
|
||||
const { path: targetPath } = (payload || {}) as { path?: string };
|
||||
if (!targetPath || targetPath.trim().length === 0) {
|
||||
return { id, type, success: false, error: 'Directory path is required' };
|
||||
}
|
||||
const folders = vscode.workspace.workspaceFolders ?? [];
|
||||
const uri = vscode.Uri.file(normalizeWindowsDriveLetter(targetPath.trim()));
|
||||
// VS Code reports workspace folder paths with lowercase Windows drive
|
||||
// letters (see pathUtils), so normalize both sides before comparing.
|
||||
const alreadyAdded = folders.some(
|
||||
(folder) => normalizeWindowsDriveLetter(folder.uri.fsPath) === uri.fsPath,
|
||||
);
|
||||
if (!alreadyAdded) {
|
||||
const updated = await vscode.workspace.updateWorkspaceFolders(folders.length, null, { uri });
|
||||
if (!updated) {
|
||||
return { id, type, success: false, error: 'Failed to add workspace folder' };
|
||||
}
|
||||
}
|
||||
return {
|
||||
id,
|
||||
type,
|
||||
success: true,
|
||||
data: { workspaceFolders: resolveWorkspaceFolders(vscode.workspace.workspaceFolders ?? []) },
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
return { id, type, success: false, error: errorMessage };
|
||||
}
|
||||
}
|
||||
|
||||
case 'vscode:command': {
|
||||
const { command, args } = (payload || {}) as { command?: string; args?: unknown[] };
|
||||
if (!command) {
|
||||
|
||||
Reference in New Issue
Block a user