diff --git a/extension.ts b/extension.ts index efd7f41..e9e2344 100644 --- a/extension.ts +++ b/extension.ts @@ -315,6 +315,22 @@ export function activate(context: vscode.ExtensionContext): ExtensionApi { vscode.commands.registerCommand('workspace-file-bookmarks.goToBookmark', () => goToBookmark(store), ), + vscode.commands.registerCommand( + 'workspace-file-bookmarks.copyBookmarkPath', + (item: BookmarkTreeItem) => copyBookmarkPath(item.bookmark), + ), + vscode.commands.registerCommand( + 'workspace-file-bookmarks.copyBookmarkRelativePath', + (item: BookmarkTreeItem) => copyBookmarkRelativePath(item.bookmark), + ), + vscode.commands.registerCommand( + 'workspace-file-bookmarks.revealBookmarkInExplorer', + (item: BookmarkTreeItem) => revealBookmarkInExplorer(item.bookmark), + ), + vscode.commands.registerCommand( + 'workspace-file-bookmarks.revealBookmarkInFileManager', + (item: BookmarkTreeItem) => revealBookmarkInFileManager(item.bookmark), + ), ); return { store, provider }; @@ -903,6 +919,26 @@ export async function removeBrokenBookmarks(store: BookmarkStore): Promise vscode.window.showInformationMessage(`Removed ${broken.length} broken bookmark(s).`); } +/** Copies the bookmarked file's absolute path to the clipboard. */ +export async function copyBookmarkPath(bookmark: Bookmark): Promise { + await vscode.env.clipboard.writeText(vscode.Uri.parse(bookmark.uri).fsPath); +} + +/** Copies the bookmarked file's workspace-relative path to the clipboard. */ +export async function copyBookmarkRelativePath(bookmark: Bookmark): Promise { + await vscode.env.clipboard.writeText(bookmark.relativePath); +} + +/** Reveals the bookmarked file in VS Code's Explorer view. */ +export async function revealBookmarkInExplorer(bookmark: Bookmark): Promise { + await vscode.commands.executeCommand('revealInExplorer', vscode.Uri.parse(bookmark.uri)); +} + +/** Reveals the bookmarked file in the OS file manager. */ +export async function revealBookmarkInFileManager(bookmark: Bookmark): Promise { + await vscode.commands.executeCommand('revealFileInOS', vscode.Uri.parse(bookmark.uri)); +} + export async function openAllInFolder(store: BookmarkStore, item: FolderGroupItem) { if (item.bookmarks.length === 0) { vscode.window.showInformationMessage(`"${item.folder.name}" has no bookmarks.`); diff --git a/package.json b/package.json index 7ce55b8..fa402bc 100644 --- a/package.json +++ b/package.json @@ -200,6 +200,26 @@ "title": "Go to Bookmark...", "category": "Workspace File Bookmarks", "icon": "$(go-to-file)" + }, + { + "command": "workspace-file-bookmarks.copyBookmarkPath", + "title": "Copy Path", + "category": "Workspace File Bookmarks" + }, + { + "command": "workspace-file-bookmarks.copyBookmarkRelativePath", + "title": "Copy Relative Path", + "category": "Workspace File Bookmarks" + }, + { + "command": "workspace-file-bookmarks.revealBookmarkInExplorer", + "title": "Reveal in Explorer", + "category": "Workspace File Bookmarks" + }, + { + "command": "workspace-file-bookmarks.revealBookmarkInFileManager", + "title": "Reveal in File Manager", + "category": "Workspace File Bookmarks" } ], "menus": { @@ -286,6 +306,26 @@ "when": "view == workspace-file-bookmarks-view && viewItem == bookmarkItem", "group": "1_modification" }, + { + "command": "workspace-file-bookmarks.revealBookmarkInExplorer", + "when": "view == workspace-file-bookmarks-view && viewItem == bookmarkItem", + "group": "2_reveal@1" + }, + { + "command": "workspace-file-bookmarks.revealBookmarkInFileManager", + "when": "view == workspace-file-bookmarks-view && viewItem == bookmarkItem", + "group": "2_reveal@2" + }, + { + "command": "workspace-file-bookmarks.copyBookmarkPath", + "when": "view == workspace-file-bookmarks-view && viewItem == bookmarkItem", + "group": "3_copy@1" + }, + { + "command": "workspace-file-bookmarks.copyBookmarkRelativePath", + "when": "view == workspace-file-bookmarks-view && viewItem == bookmarkItem", + "group": "3_copy@2" + }, { "command": "workspace-file-bookmarks.openAllInFolder", "when": "view == workspace-file-bookmarks-view && viewItem == bookmarkFolder", diff --git a/test/activate.test.ts b/test/activate.test.ts index 24a4d11..142b970 100644 --- a/test/activate.test.ts +++ b/test/activate.test.ts @@ -15,8 +15,8 @@ describe('activate', () => { 'workspace-file-bookmarks-view', expect.objectContaining({ canSelectMany: true }), ); - expect(commands.registerCommand).toHaveBeenCalledTimes(25); - expect((context as any).subscriptions).toHaveLength(26); // tree view + 25 commands + expect(commands.registerCommand).toHaveBeenCalledTimes(29); + expect((context as any).subscriptions).toHaveLength(30); // tree view + 29 commands }); it('exercises every registered command handler at least once', async () => { @@ -78,8 +78,13 @@ describe('activate', () => { await handlers.get('workspace-file-bookmarks.importBookmarks')?.(); window.showQuickPick.mockResolvedValue(undefined); await handlers.get('workspace-file-bookmarks.goToBookmark')?.(); + await handlers.get('workspace-file-bookmarks.copyBookmarkPath')?.({ bookmark }); + await handlers.get('workspace-file-bookmarks.copyBookmarkRelativePath')?.({ bookmark }); + await handlers.get('workspace-file-bookmarks.revealBookmarkInExplorer')?.({ bookmark }); + await handlers.get('workspace-file-bookmarks.revealBookmarkInFileManager')?.({ bookmark }); - expect(handlers.size).toBe(25); + expect(commands.registerCommand).toHaveBeenCalledTimes(29); + expect((context as any).subscriptions).toHaveLength(30); // tree view + 29 commands }); }); diff --git a/test/commands.test.ts b/test/commands.test.ts index bb089b5..99065d4 100644 --- a/test/commands.test.ts +++ b/test/commands.test.ts @@ -5,6 +5,8 @@ import { addActiveSelectionBookmark, addBookmarksForUris, addBookmarksToFolder, + copyBookmarkPath, + copyBookmarkRelativePath, createFolder, createSubfolder, deleteFolder, @@ -24,11 +26,13 @@ import { removeBrokenBookmarks, renameFolder, resolveBookmarkTargets, + revealBookmarkInExplorer, + revealBookmarkInFileManager, type Bookmark, type BookmarkFolder, } from '../extension'; import { createFakeContext } from './fakeContext'; -import { Position, Selection, Uri, window, workspace } from './vscode-mock'; +import { Position, Selection, Uri, commands, env, window, workspace } from './vscode-mock'; import type { InputBox } from './vscode-mock'; function newStore() { @@ -1097,3 +1101,35 @@ describe('removeBrokenBookmarks', () => { expect(window.showWarningMessage).not.toHaveBeenCalled(); }); }); + +describe('copy path / reveal actions', () => { + it('copies the absolute path of the bookmarked file', async () => { + await copyBookmarkPath(makeBookmark({ uri: 'file:///repo/src/a.ts' })); + + expect(env.clipboard.writeText).toHaveBeenCalledWith('/repo/src/a.ts'); + }); + + it('copies the workspace-relative path of the bookmarked file', async () => { + await copyBookmarkRelativePath(makeBookmark({ relativePath: 'src/a.ts' })); + + expect(env.clipboard.writeText).toHaveBeenCalledWith('src/a.ts'); + }); + + it("reveals the bookmarked file in VS Code's Explorer", async () => { + await revealBookmarkInExplorer(makeBookmark({ uri: 'file:///repo/src/a.ts' })); + + expect(commands.executeCommand).toHaveBeenCalledWith( + 'revealInExplorer', + expect.objectContaining({ fsPath: '/repo/src/a.ts' }), + ); + }); + + it('reveals the bookmarked file in the OS file manager', async () => { + await revealBookmarkInFileManager(makeBookmark({ uri: 'file:///repo/src/a.ts' })); + + expect(commands.executeCommand).toHaveBeenCalledWith( + 'revealFileInOS', + expect.objectContaining({ fsPath: '/repo/src/a.ts' }), + ); + }); +}); diff --git a/test/e2e/extension.e2e.test.ts b/test/e2e/extension.e2e.test.ts index 5764275..dcab61d 100644 --- a/test/e2e/extension.e2e.test.ts +++ b/test/e2e/extension.e2e.test.ts @@ -69,6 +69,10 @@ describe('Workspace File Bookmarks (e2e)', () => { 'workspace-file-bookmarks.exportBookmarks', 'workspace-file-bookmarks.importBookmarks', 'workspace-file-bookmarks.goToBookmark', + 'workspace-file-bookmarks.copyBookmarkPath', + 'workspace-file-bookmarks.copyBookmarkRelativePath', + 'workspace-file-bookmarks.revealBookmarkInExplorer', + 'workspace-file-bookmarks.revealBookmarkInFileManager', ]) { assert.ok(commands.includes(id), `expected command "${id}" to be registered`); } @@ -373,4 +377,28 @@ describe('Workspace File Bookmarks (e2e)', () => { ); assert.equal(store.getAllBookmarks().length, 0); }); + + it('copies the absolute and relative path of a bookmark to the clipboard', async () => { + const { store } = await getApi(); + const document = await vscode.workspace.openTextDocument(fixtureUri('src/sample-a.ts')); + await vscode.window.showTextDocument(document); + await vscode.commands.executeCommand('workspace-file-bookmarks.addBookmark'); + const item = { bookmark: store.getAllBookmarks()[0] }; + + await vscode.commands.executeCommand('workspace-file-bookmarks.copyBookmarkPath', item); + assert.equal(await vscode.env.clipboard.readText(), document.uri.fsPath); + + await vscode.commands.executeCommand('workspace-file-bookmarks.copyBookmarkRelativePath', item); + assert.equal(await vscode.env.clipboard.readText(), 'src/sample-a.ts'); + }); + + it("reveals a bookmark in VS Code's Explorer without error", async () => { + const { store } = await getApi(); + const document = await vscode.workspace.openTextDocument(fixtureUri('src/sample-a.ts')); + await vscode.window.showTextDocument(document); + await vscode.commands.executeCommand('workspace-file-bookmarks.addBookmark'); + const item = { bookmark: store.getAllBookmarks()[0] }; + + await vscode.commands.executeCommand('workspace-file-bookmarks.revealBookmarkInExplorer', item); + }); }); diff --git a/test/vscode-mock.ts b/test/vscode-mock.ts index 6e45146..4c7c49d 100644 --- a/test/vscode-mock.ts +++ b/test/vscode-mock.ts @@ -180,3 +180,9 @@ export const commands = { registerCommand: vi.fn(() => ({ dispose: () => {} })), executeCommand: vi.fn(), }; + +export const env = { + clipboard: { + writeText: vi.fn(() => Promise.resolve()), + }, +};