From e9ce511b6564c88ffdce2874309a2ca10452df84 Mon Sep 17 00:00:00 2001 From: bgaeddert Date: Thu, 2 Apr 2026 06:06:39 -0500 Subject: [PATCH 1/7] feat: add Copy Reference context menu for databases and collections Adds a "Copy Reference" right-click option to database and collection nodes in the sidebar. Database copies the db name, collection copies db.collection dot notation. --- package.json | 32 ++++++++++++++++++ src/commands/copyReference/copyReference.ts | 36 +++++++++++++++++++++ src/documentdb/ClustersExtension.ts | 10 ++++++ 3 files changed, 78 insertions(+) create mode 100644 src/commands/copyReference/copyReference.ts diff --git a/package.json b/package.json index ed394f432..dba596e7b 100644 --- a/package.json +++ b/package.json @@ -533,6 +533,18 @@ "category": "DocumentDB", "command": "vscode-documentdb.command.pasteCollection", "title": "Paste Collection…" + }, + { + "//": "Copy Database Reference", + "category": "DocumentDB", + "command": "vscode-documentdb.command.copyDatabaseReference", + "title": "Copy Reference" + }, + { + "//": "Copy Collection Reference", + "category": "DocumentDB", + "command": "vscode-documentdb.command.copyCollectionReference", + "title": "Copy Reference" } ], "submenus": [ @@ -864,6 +876,18 @@ "command": "vscode-documentdb.command.pasteCollection", "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_collection\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", "group": "3@4" + }, + { + "//": "[Database] Copy Reference", + "command": "vscode-documentdb.command.copyDatabaseReference", + "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_database\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", + "group": "6@1" + }, + { + "//": "[Collection] Copy Reference", + "command": "vscode-documentdb.command.copyCollectionReference", + "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_collection\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", + "group": "6@1" } ], "explorer/context": [], @@ -928,6 +952,14 @@ "command": "vscode-documentdb.command.azureResourcesView.addConnectionToConnectionsView", "when": "never" }, + { + "command": "vscode-documentdb.command.copyDatabaseReference", + "when": "never" + }, + { + "command": "vscode-documentdb.command.copyCollectionReference", + "when": "never" + }, { "command": "vscode-documentdb.command.copyConnectionString", "when": "never" diff --git a/src/commands/copyReference/copyReference.ts b/src/commands/copyReference/copyReference.ts new file mode 100644 index 000000000..1579d456b --- /dev/null +++ b/src/commands/copyReference/copyReference.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { type IActionContext } from '@microsoft/vscode-azext-utils'; +import * as l10n from '@vscode/l10n'; +import * as vscode from 'vscode'; +import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; +import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; + +export async function copyDatabaseReference( + _context: IActionContext, + node: DatabaseItem, +): Promise { + if (!node) { + throw new Error(l10n.t('No node selected.')); + } + + const reference = node.databaseInfo.name; + await vscode.env.clipboard.writeText(reference); + void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard')); +} + +export async function copyCollectionReference( + _context: IActionContext, + node: CollectionItem, +): Promise { + if (!node) { + throw new Error(l10n.t('No node selected.')); + } + + const reference = `${node.databaseInfo.name}.${node.collectionInfo.name}`; + await vscode.env.clipboard.writeText(reference); + void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard')); +} diff --git a/src/documentdb/ClustersExtension.ts b/src/documentdb/ClustersExtension.ts index 59f9422e8..655402f69 100644 --- a/src/documentdb/ClustersExtension.ts +++ b/src/documentdb/ClustersExtension.ts @@ -29,6 +29,7 @@ import { renameConnection } from '../commands/connections-view/renameConnection/ import { renameFolder } from '../commands/connections-view/renameFolder/renameFolder'; import { copyCollection } from '../commands/copyCollection/copyCollection'; import { copyAzureConnectionString } from '../commands/copyConnectionString/copyConnectionString'; +import { copyCollectionReference, copyDatabaseReference } from '../commands/copyReference/copyReference'; import { createCollection } from '../commands/createCollection/createCollection'; import { createAzureDatabase } from '../commands/createDatabase/createDatabase'; import { createMongoDocument } from '../commands/createDocument/createDocument'; @@ -387,6 +388,15 @@ export class ClustersExtension implements vscode.Disposable { withTreeNodeCommandCorrelation(deleteAzureDatabase), ); + registerCommandWithTreeNodeUnwrapping( + 'vscode-documentdb.command.copyDatabaseReference', + withTreeNodeCommandCorrelation(copyDatabaseReference), + ); + registerCommandWithTreeNodeUnwrapping( + 'vscode-documentdb.command.copyCollectionReference', + withTreeNodeCommandCorrelation(copyCollectionReference), + ); + registerCommandWithTreeNodeUnwrapping( 'vscode-documentdb.command.hideIndex', withTreeNodeCommandCorrelation(hideIndex), From 094a2dc0d1cfe76da6f544dd19e507e635cafe3e Mon Sep 17 00:00:00 2001 From: bgaeddert Date: Thu, 2 Apr 2026 06:43:15 -0500 Subject: [PATCH 2/7] fix: update l10n bundle with new localization key for Copy Reference --- l10n/bundle.l10n.json | 1 + 1 file changed, 1 insertion(+) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index a4493f78a..7b834da67 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -986,6 +986,7 @@ "The name must be between {0} and {1} characters.": "The name must be between {0} and {1} characters.", "The output window may contain additional information.": "The output window may contain additional information.", "The process exited prematurely.": "The process exited prematurely.", + "The reference has been copied to the clipboard": "The reference has been copied to the clipboard", "The selected authentication method is not supported.": "The selected authentication method is not supported.", "The selected connection has been removed.": "The selected connection has been removed.", "The selected folder has been removed.": "The selected folder has been removed.", From 34c144bf9372f71fada685d5f2ffd39d5db4009c Mon Sep 17 00:00:00 2001 From: bgaeddert Date: Thu, 2 Apr 2026 07:27:07 -0500 Subject: [PATCH 3/7] style: fix prettier formatting in copyReference.ts --- src/commands/copyReference/copyReference.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/commands/copyReference/copyReference.ts b/src/commands/copyReference/copyReference.ts index 1579d456b..3dfb555fe 100644 --- a/src/commands/copyReference/copyReference.ts +++ b/src/commands/copyReference/copyReference.ts @@ -9,10 +9,7 @@ import * as vscode from 'vscode'; import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; -export async function copyDatabaseReference( - _context: IActionContext, - node: DatabaseItem, -): Promise { +export async function copyDatabaseReference(_context: IActionContext, node: DatabaseItem): Promise { if (!node) { throw new Error(l10n.t('No node selected.')); } @@ -22,10 +19,7 @@ export async function copyDatabaseReference( void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard')); } -export async function copyCollectionReference( - _context: IActionContext, - node: CollectionItem, -): Promise { +export async function copyCollectionReference(_context: IActionContext, node: CollectionItem): Promise { if (!node) { throw new Error(l10n.t('No node selected.')); } From 32e6d3ebb0fda984e8ff9a53141c063cc8438aa1 Mon Sep 17 00:00:00 2001 From: bgaeddert Date: Thu, 2 Apr 2026 07:48:51 -0500 Subject: [PATCH 4/7] refactor: consolidate copy reference into shared helper and differentiate titles Extracts shared logic into copyReferenceInternal helper. Renames command titles to "Copy Database Reference" and "Copy Collection Reference" to avoid duplicate labels in VS Code command/keybinding lists. --- l10n/bundle.l10n.json | 2 +- package.json | 4 ++-- src/commands/copyReference/copyReference.ts | 21 +++++++++++---------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 7b834da67..69599aa20 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -986,7 +986,7 @@ "The name must be between {0} and {1} characters.": "The name must be between {0} and {1} characters.", "The output window may contain additional information.": "The output window may contain additional information.", "The process exited prematurely.": "The process exited prematurely.", - "The reference has been copied to the clipboard": "The reference has been copied to the clipboard", + "The reference has been copied to the clipboard.": "The reference has been copied to the clipboard.", "The selected authentication method is not supported.": "The selected authentication method is not supported.", "The selected connection has been removed.": "The selected connection has been removed.", "The selected folder has been removed.": "The selected folder has been removed.", diff --git a/package.json b/package.json index dba596e7b..6788fb90e 100644 --- a/package.json +++ b/package.json @@ -538,13 +538,13 @@ "//": "Copy Database Reference", "category": "DocumentDB", "command": "vscode-documentdb.command.copyDatabaseReference", - "title": "Copy Reference" + "title": "Copy Database Reference" }, { "//": "Copy Collection Reference", "category": "DocumentDB", "command": "vscode-documentdb.command.copyCollectionReference", - "title": "Copy Reference" + "title": "Copy Collection Reference" } ], "submenus": [ diff --git a/src/commands/copyReference/copyReference.ts b/src/commands/copyReference/copyReference.ts index 3dfb555fe..0a8180887 100644 --- a/src/commands/copyReference/copyReference.ts +++ b/src/commands/copyReference/copyReference.ts @@ -9,22 +9,23 @@ import * as vscode from 'vscode'; import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; -export async function copyDatabaseReference(_context: IActionContext, node: DatabaseItem): Promise { +async function copyReferenceInternal( + node: T | undefined, + getReference: (node: T) => string, +): Promise { if (!node) { throw new Error(l10n.t('No node selected.')); } - const reference = node.databaseInfo.name; + const reference = getReference(node); await vscode.env.clipboard.writeText(reference); - void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard')); + void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard.')); } -export async function copyCollectionReference(_context: IActionContext, node: CollectionItem): Promise { - if (!node) { - throw new Error(l10n.t('No node selected.')); - } +export async function copyDatabaseReference(_context: IActionContext, node: DatabaseItem): Promise { + await copyReferenceInternal(node, (n) => n.databaseInfo.name); +} - const reference = `${node.databaseInfo.name}.${node.collectionInfo.name}`; - await vscode.env.clipboard.writeText(reference); - void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard')); +export async function copyCollectionReference(_context: IActionContext, node: CollectionItem): Promise { + await copyReferenceInternal(node, (n) => `${n.databaseInfo.name}.${n.collectionInfo.name}`); } From 0b888990ec51448846c87fadd207243e13035829 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 10:57:49 +0200 Subject: [PATCH 5/7] faet: new skill for external PR reviews --- .github/skills/review-external-pr/SKILL.md | 101 +++++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 .github/skills/review-external-pr/SKILL.md diff --git a/.github/skills/review-external-pr/SKILL.md b/.github/skills/review-external-pr/SKILL.md new file mode 100644 index 000000000..6a4cc134b --- /dev/null +++ b/.github/skills/review-external-pr/SKILL.md @@ -0,0 +1,101 @@ +--- +name: review-external-pr +description: Prepare an external contributor's PR for maintainer review by redirecting it into a dedicated review branch, then merging and creating a new finalization PR targeting next. Use when triaging/reviewing contributor PRs, merging external PRs with maintainer changes, or setting up a review workflow for incoming community contributions. +--- + +# Review External PR Workflow + +Redirects an external contributor's PR into a `reviews/` staging branch so a maintainer can inspect, add changes, then merge everything into `next` cleanly. + +## When to Use + +- An external contributor opened a PR targeting `next` and you want to add changes before merging +- You want to formally review and finalize a community contribution +- You want the contributor to get proper merge credit while still controlling what lands in `next` + +## Workflow Steps + +### 1. Gather PR Info + +```bash +gh pr view --json title,author,headRefName,baseRefName,body +``` + +Note the **PR number**, **title**, and **author login** — you'll need them for branch naming and PR descriptions. + +### 2. Create the Review Branch + +Branch naming format: `reviews/-original-pr-` + +```bash +git fetch origin +git checkout -b reviews/-original-pr- origin/next +git push origin reviews/-original-pr- +``` + +Example: `reviews/copy-reference-original-pr-545` + +### 3. Retarget the Contributor's PR + +> ⚠️ **Known issue**: `gh pr edit --base` may emit a deprecation warning about Projects (classic). This is a cosmetic warning only — the base branch change succeeds regardless. Verify with `gh pr view --json baseRefName`. + +```bash +gh pr edit --base reviews/-original-pr- +``` + +Verify: + +```bash +gh pr view --json baseRefName +``` + +### 4. Merge the Contributor's PR + +Once the base is updated and the PR is ready: + +```bash +gh pr merge --squash +``` + +Or approve + merge via the GitHub UI to trigger any required status checks. + +### 5. Create the Finalization PR + +Pull the merged review branch, then open a new PR from it to `next`: + +```bash +git checkout reviews/-original-pr- +git pull origin reviews/-original-pr- +``` + +Create the PR: + +```bash +gh pr create \ + --base next \ + --head reviews/-original-pr- \ + --title " [reviewed]" \ + --body "This PR finalizes the review of the contribution originally submitted by @ in #. + +Original PR: " +``` + +### 6. Comment on the Original PR + +Go back to the contributor's original (now merged) PR and leave a comment linking to the finalization PR: + +```bash +gh pr comment \ + --body "Thank you for the contribution! The review is continuing in # where maintainer changes will be finalized before merging to \`next\`." +``` + +## Summary + +| Step | Action | Result | +| ---- | ------------------------------------------ | ----------------------------------------- | +| 1 | Gather PR info | Know PR number, title, author | +| 2 | Create `reviews/...` branch off `next` | Staging branch ready | +| 3 | Retarget contributor's PR to review branch | Their diff is scoped to review branch | +| 4 | Merge contributor's PR | Contributor gets merge credit | +| 5 | Create finalization PR to `next` | Maintainer controls what lands in `next` | +| 6 | Comment on original PR with link to new PR | Contributor is informed, thread is linked | From 1a596eb0312072a5d54cdc5103433c5670f83a3f Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 12:52:28 +0200 Subject: [PATCH 6/7] feat: implement unified Copy Reference command for databases, collections, and indexes --- l10n/bundle.l10n.json | 11 +- package.json | 32 ++- src/commands/copyReference/copyReference.ts | 211 ++++++++++++++++++-- src/documentdb/ClustersExtension.ts | 10 +- 4 files changed, 225 insertions(+), 39 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index 69599aa20..dd19183d3 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -282,12 +282,15 @@ "Connection: {connectionName}": "Connection: {connectionName}", "Connections have moved": "Connections have moved", "Continue": "Continue", + "Copied to clipboard": "Copied to clipboard", "Copy \"{sourceCollection}\" from \"{sourceDatabase}\" to \"{targetDatabase}/{targetCollection}\"": "Copy \"{sourceCollection}\" from \"{sourceDatabase}\" to \"{targetDatabase}/{targetCollection}\"", "Copy index definitions from source collection?": "Copy index definitions from source collection?", "Copy index definitions from source to target collection.": "Copy index definitions from source to target collection.", "Copy Indexes: {yesNoValue}": "Copy Indexes: {yesNoValue}", "Copy only documents without recreating indexes.": "Copy only documents without recreating indexes.", "Copy operation cancelled.": "Copy operation cancelled.", + "Copy Reference: {0}": "Copy Reference: {0}", + "Copy Reference: {0}.{1}": "Copy Reference: {0}.{1}", "Copy with password": "Copy with password", "Copy without password": "Copy without password", "Copy-and-Merge": "Copy-and-Merge", @@ -639,6 +642,7 @@ "JSON View": "JSON View", "Keep-alive timeout exceeded": "Keep-alive timeout exceeded", "Keep-alive timeout exceeded: stream has been running for {0} seconds (limit: {1} seconds)": "Keep-alive timeout exceeded: stream has been running for {0} seconds (limit: {1} seconds)", + "Key Definition": "Key Definition", "Keys Examined": "Keys Examined", "Large Collection Copy Operation": "Large Collection Copy Operation", "Learn more": "Learn more", @@ -695,6 +699,8 @@ "Move to top level": "Move to top level", "Moved {0} item(s) to \"{1}\".": "Moved {0} item(s) to \"{1}\".", "N/A": "N/A", + "Name": "Name", + "Namespace": "Namespace", "New Connection": "New Connection", "New connection has been added to your DocumentDB Connections.": "New connection has been added to your DocumentDB Connections.", "New connection has been added.": "New connection has been added.", @@ -789,6 +795,7 @@ "Project": "Project", "Project: Specify which fields to include or exclude": "Project: Specify which fields to include or exclude", "Provider \"{0}\" does not have resource type \"{1}\".": "Provider \"{0}\" does not have resource type \"{1}\".", + "Qualified Name": "Qualified Name", "Query Efficiency Analysis": "Query Efficiency Analysis", "Query Execution Failed": "Query Execution Failed", "Query generation failed": "Query generation failed", @@ -849,6 +856,7 @@ "See output for more details.": "See output for more details.", "Select {0}": "Select {0}", "Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}", + "Select a format to copy": "Select a format to copy", "Select a location for new resources.": "Select a location for new resources.", "Select a tenant for Microsoft Entra ID authentication": "Select a tenant for Microsoft Entra ID authentication", "Select a workspace folder": "Select a workspace folder", @@ -874,6 +882,8 @@ "SHARD_MERGE · {0} shards": "SHARD_MERGE · {0} shards", "SHARD_MERGE · {0} shards · {1} docs · {2}ms": "SHARD_MERGE · {0} shards · {1} docs · {2}ms", "Shard: {0}": "Shard: {0}", + "Shell Command": "Shell Command", + "Shell Reference": "Shell Reference", "Show Output": "Show Output", "Show Stage Details": "Show Stage Details", "Sign in to additional accounts or authenticate with other tenants to see more options.": "Sign in to additional accounts or authenticate with other tenants to see more options.", @@ -986,7 +996,6 @@ "The name must be between {0} and {1} characters.": "The name must be between {0} and {1} characters.", "The output window may contain additional information.": "The output window may contain additional information.", "The process exited prematurely.": "The process exited prematurely.", - "The reference has been copied to the clipboard.": "The reference has been copied to the clipboard.", "The selected authentication method is not supported.": "The selected authentication method is not supported.", "The selected connection has been removed.": "The selected connection has been removed.", "The selected folder has been removed.": "The selected folder has been removed.", diff --git a/package.json b/package.json index 6788fb90e..ba6272d11 100644 --- a/package.json +++ b/package.json @@ -535,16 +535,10 @@ "title": "Paste Collection…" }, { - "//": "Copy Database Reference", + "//": "Copy Reference", "category": "DocumentDB", - "command": "vscode-documentdb.command.copyDatabaseReference", - "title": "Copy Database Reference" - }, - { - "//": "Copy Collection Reference", - "category": "DocumentDB", - "command": "vscode-documentdb.command.copyCollectionReference", - "title": "Copy Collection Reference" + "command": "vscode-documentdb.command.copyReference", + "title": "Copy Reference…" } ], "submenus": [ @@ -879,15 +873,21 @@ }, { "//": "[Database] Copy Reference", - "command": "vscode-documentdb.command.copyDatabaseReference", + "command": "vscode-documentdb.command.copyReference", "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_database\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", - "group": "6@1" + "group": "yheAlmostLastGroup@1" }, { "//": "[Collection] Copy Reference", - "command": "vscode-documentdb.command.copyCollectionReference", + "command": "vscode-documentdb.command.copyReference", "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_collection\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", - "group": "6@1" + "group": "yheAlmostLastGroup@1" + }, + { + "//": "[Index] Copy Reference", + "command": "vscode-documentdb.command.copyReference", + "when": "view =~ /connectionsView|discoveryView|azure(ResourceGroups|FocusView)/ && viewItem =~ /\\btreeitem_index\\b/i && viewItem =~ /\\bexperience_(documentDB|mongoRU)\\b/i", + "group": "yheAlmostLastGroup@1" } ], "explorer/context": [], @@ -953,11 +953,7 @@ "when": "never" }, { - "command": "vscode-documentdb.command.copyDatabaseReference", - "when": "never" - }, - { - "command": "vscode-documentdb.command.copyCollectionReference", + "command": "vscode-documentdb.command.copyReference", "when": "never" }, { diff --git a/src/commands/copyReference/copyReference.ts b/src/commands/copyReference/copyReference.ts index 0a8180887..a9cfbae98 100644 --- a/src/commands/copyReference/copyReference.ts +++ b/src/commands/copyReference/copyReference.ts @@ -6,26 +6,211 @@ import { type IActionContext } from '@microsoft/vscode-azext-utils'; import * as l10n from '@vscode/l10n'; import * as vscode from 'vscode'; -import { type CollectionItem } from '../../tree/documentdb/CollectionItem'; +import { CollectionItem } from '../../tree/documentdb/CollectionItem'; import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; +import { IndexItem } from '../../tree/documentdb/IndexItem'; -async function copyReferenceInternal( - node: T | undefined, - getReference: (node: T) => string, +interface CopyReferenceOption extends vscode.QuickPickItem { + value: string; +} + +function formatIndexKey(key: Record): string { + const entries = Object.entries(key) + .map(([field, order]) => `${field}: ${order}`) + .join(', '); + return `{ ${entries} }`; +} + +/** + * Returns true if a name requires quoting (cannot be used in dot-notation). + * A name is safe for dot-notation only if it matches a valid JS identifier. + */ +function needsQuoting(name: string): boolean { + return !/^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name); +} + +function escapeDoubleQuotes(value: string): string { + return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"'); +} + +function shellCollectionRef(collName: string): string { + return needsQuoting(collName) ? `db.getCollection("${escapeDoubleQuotes(collName)}")` : `db.${collName}`; +} + +function getClusterHost(connectionString: string | undefined): string | undefined { + if (!connectionString) { + return undefined; + } + + try { + const url = new URL(connectionString); + return url.host; + } catch { + return undefined; + } +} + +function getDatabaseOptions(node: DatabaseItem): CopyReferenceOption[] { + const dbName = node.databaseInfo.name; + const host = getClusterHost(node.cluster.connectionString); + + const options: CopyReferenceOption[] = [ + { + label: l10n.t('Name'), + detail: dbName, + value: dbName, + }, + { + label: l10n.t('Shell Command'), + detail: `use ${dbName}`, + value: `use ${dbName}`, + }, + ]; + + if (host) { + options.push({ + label: l10n.t('Qualified Name'), + detail: `${host}/${dbName}`, + value: `${host}/${dbName}`, + }); + } + + return options; +} + +function getCollectionOptions(node: CollectionItem): CopyReferenceOption[] { + const dbName = node.databaseInfo.name; + const collName = node.collectionInfo.name; + const quoted = needsQuoting(collName) || needsQuoting(dbName); + const escapedCollName = escapeDoubleQuotes(collName); + + const options: CopyReferenceOption[] = [ + { + label: l10n.t('Name'), + detail: collName, + value: collName, + }, + ]; + + if (!quoted) { + options.push({ + label: l10n.t('Namespace'), + detail: `${dbName}.${collName}`, + value: `${dbName}.${collName}`, + }); + } + + if (!quoted) { + options.push({ + label: l10n.t('Shell Reference'), + detail: `db.${collName}`, + value: `db.${collName}`, + }); + } + + options.push({ + label: l10n.t('Shell Command'), + detail: `db.getCollection("${escapedCollName}")`, + value: `db.getCollection("${escapedCollName}")`, + }); + + return options; +} + +function getIndexOptions(node: IndexItem): CopyReferenceOption[] { + const indexName = node.indexInfo.name; + const collName = node.collectionInfo.name; + + const options: CopyReferenceOption[] = [ + { + label: l10n.t('Name'), + detail: indexName, + value: indexName, + }, + ]; + + if (node.indexInfo.key) { + const keyDef = formatIndexKey(node.indexInfo.key); + + options.push({ + label: l10n.t('Key Definition'), + detail: keyDef, + value: keyDef, + }); + + const collRef = shellCollectionRef(collName); + const escapedIndexName = escapeDoubleQuotes(indexName); + + options.push({ + label: l10n.t('Shell Command'), + detail: `${collRef}.getIndexes().find(i => i.name === "${escapedIndexName}")`, + value: `${collRef}.getIndexes().find(i => i.name === "${escapedIndexName}")`, + }); + } + + return options; +} + +function getOptionsForNode(node: DatabaseItem | CollectionItem | IndexItem): { + title: string; + options: CopyReferenceOption[]; +} { + if (node instanceof IndexItem) { + return { + title: l10n.t('Copy Reference: {0}', node.indexInfo.name), + options: getIndexOptions(node), + }; + } + + if (node instanceof CollectionItem) { + return { + title: l10n.t('Copy Reference: {0}.{1}', node.databaseInfo.name, node.collectionInfo.name), + options: getCollectionOptions(node), + }; + } + + return { + title: l10n.t('Copy Reference: {0}', node.databaseInfo.name), + options: getDatabaseOptions(node), + }; +} + +export async function copyReference( + _context: IActionContext, + node: DatabaseItem | CollectionItem | IndexItem, ): Promise { if (!node) { throw new Error(l10n.t('No node selected.')); } - const reference = getReference(node); - await vscode.env.clipboard.writeText(reference); - void vscode.window.showInformationMessage(l10n.t('The reference has been copied to the clipboard.')); -} + const { title, options } = getOptionsForNode(node); -export async function copyDatabaseReference(_context: IActionContext, node: DatabaseItem): Promise { - await copyReferenceInternal(node, (n) => n.databaseInfo.name); -} + const picker = vscode.window.createQuickPick(); + picker.title = title; + picker.placeholder = l10n.t('Select a format to copy'); + picker.items = options; + picker.matchOnDetail = true; + + // Workaround: setting sortByLabel to false via the options object is not available + // in the createQuickPick API, but we can suppress persistence by not setting a value + // for the quickpick's value property, which avoids reordering. + + const result = await new Promise((resolve) => { + picker.onDidAccept(() => { + resolve(picker.selectedItems[0] as CopyReferenceOption | undefined); + picker.dispose(); + }); + picker.onDidHide(() => { + resolve(undefined); + picker.dispose(); + }); + picker.show(); + }); + + if (!result) { + return; + } -export async function copyCollectionReference(_context: IActionContext, node: CollectionItem): Promise { - await copyReferenceInternal(node, (n) => `${n.databaseInfo.name}.${n.collectionInfo.name}`); + await vscode.env.clipboard.writeText(result.value); + void vscode.window.showInformationMessage(l10n.t('Copied to clipboard')); } diff --git a/src/documentdb/ClustersExtension.ts b/src/documentdb/ClustersExtension.ts index 655402f69..6e7108f2d 100644 --- a/src/documentdb/ClustersExtension.ts +++ b/src/documentdb/ClustersExtension.ts @@ -29,7 +29,7 @@ import { renameConnection } from '../commands/connections-view/renameConnection/ import { renameFolder } from '../commands/connections-view/renameFolder/renameFolder'; import { copyCollection } from '../commands/copyCollection/copyCollection'; import { copyAzureConnectionString } from '../commands/copyConnectionString/copyConnectionString'; -import { copyCollectionReference, copyDatabaseReference } from '../commands/copyReference/copyReference'; +import { copyReference } from '../commands/copyReference/copyReference'; import { createCollection } from '../commands/createCollection/createCollection'; import { createAzureDatabase } from '../commands/createDatabase/createDatabase'; import { createMongoDocument } from '../commands/createDocument/createDocument'; @@ -389,12 +389,8 @@ export class ClustersExtension implements vscode.Disposable { ); registerCommandWithTreeNodeUnwrapping( - 'vscode-documentdb.command.copyDatabaseReference', - withTreeNodeCommandCorrelation(copyDatabaseReference), - ); - registerCommandWithTreeNodeUnwrapping( - 'vscode-documentdb.command.copyCollectionReference', - withTreeNodeCommandCorrelation(copyCollectionReference), + 'vscode-documentdb.command.copyReference', + withTreeNodeCommandCorrelation(copyReference), ); registerCommandWithTreeNodeUnwrapping( From 3a4e6e2704e98cf4b1c0d5a5311d1242181ed7f5 Mon Sep 17 00:00:00 2001 From: Tomasz Naumowicz Date: Thu, 16 Apr 2026 13:03:56 +0200 Subject: [PATCH 7/7] chore: simplified quickpick / wizard code --- l10n/bundle.l10n.json | 1 - src/commands/copyReference/copyReference.ts | 115 +++++--------------- 2 files changed, 26 insertions(+), 90 deletions(-) diff --git a/l10n/bundle.l10n.json b/l10n/bundle.l10n.json index dd19183d3..ee9aae2b7 100644 --- a/l10n/bundle.l10n.json +++ b/l10n/bundle.l10n.json @@ -856,7 +856,6 @@ "See output for more details.": "See output for more details.", "Select {0}": "Select {0}", "Select {mongoExecutableFileName}": "Select {mongoExecutableFileName}", - "Select a format to copy": "Select a format to copy", "Select a location for new resources.": "Select a location for new resources.", "Select a tenant for Microsoft Entra ID authentication": "Select a tenant for Microsoft Entra ID authentication", "Select a workspace folder": "Select a workspace folder", diff --git a/src/commands/copyReference/copyReference.ts b/src/commands/copyReference/copyReference.ts index a9cfbae98..39b7e74d6 100644 --- a/src/commands/copyReference/copyReference.ts +++ b/src/commands/copyReference/copyReference.ts @@ -10,8 +10,11 @@ import { CollectionItem } from '../../tree/documentdb/CollectionItem'; import { type DatabaseItem } from '../../tree/documentdb/DatabaseItem'; import { IndexItem } from '../../tree/documentdb/IndexItem'; -interface CopyReferenceOption extends vscode.QuickPickItem { - value: string; +interface CopyReferenceOption { + id: string; + label: string; + detail: string; + alwaysShow: true; } function formatIndexKey(key: Record): string { @@ -50,29 +53,18 @@ function getClusterHost(connectionString: string | undefined): string | undefine } } +function opt(label: string, value: string): CopyReferenceOption { + return { id: value, label, detail: value, alwaysShow: true }; +} + function getDatabaseOptions(node: DatabaseItem): CopyReferenceOption[] { const dbName = node.databaseInfo.name; const host = getClusterHost(node.cluster.connectionString); - const options: CopyReferenceOption[] = [ - { - label: l10n.t('Name'), - detail: dbName, - value: dbName, - }, - { - label: l10n.t('Shell Command'), - detail: `use ${dbName}`, - value: `use ${dbName}`, - }, - ]; + const options: CopyReferenceOption[] = [opt(l10n.t('Name'), dbName), opt(l10n.t('Shell Command'), `use ${dbName}`)]; if (host) { - options.push({ - label: l10n.t('Qualified Name'), - detail: `${host}/${dbName}`, - value: `${host}/${dbName}`, - }); + options.push(opt(l10n.t('Qualified Name'), `${host}/${dbName}`)); } return options; @@ -84,35 +76,14 @@ function getCollectionOptions(node: CollectionItem): CopyReferenceOption[] { const quoted = needsQuoting(collName) || needsQuoting(dbName); const escapedCollName = escapeDoubleQuotes(collName); - const options: CopyReferenceOption[] = [ - { - label: l10n.t('Name'), - detail: collName, - value: collName, - }, - ]; - - if (!quoted) { - options.push({ - label: l10n.t('Namespace'), - detail: `${dbName}.${collName}`, - value: `${dbName}.${collName}`, - }); - } + const options: CopyReferenceOption[] = [opt(l10n.t('Name'), collName)]; if (!quoted) { - options.push({ - label: l10n.t('Shell Reference'), - detail: `db.${collName}`, - value: `db.${collName}`, - }); + options.push(opt(l10n.t('Namespace'), `${dbName}.${collName}`)); + options.push(opt(l10n.t('Shell Reference'), `db.${collName}`)); } - options.push({ - label: l10n.t('Shell Command'), - detail: `db.getCollection("${escapedCollName}")`, - value: `db.getCollection("${escapedCollName}")`, - }); + options.push(opt(l10n.t('Shell Command'), `db.getCollection("${escapedCollName}")`)); return options; } @@ -121,31 +92,17 @@ function getIndexOptions(node: IndexItem): CopyReferenceOption[] { const indexName = node.indexInfo.name; const collName = node.collectionInfo.name; - const options: CopyReferenceOption[] = [ - { - label: l10n.t('Name'), - detail: indexName, - value: indexName, - }, - ]; + const options: CopyReferenceOption[] = [opt(l10n.t('Name'), indexName)]; if (node.indexInfo.key) { const keyDef = formatIndexKey(node.indexInfo.key); - - options.push({ - label: l10n.t('Key Definition'), - detail: keyDef, - value: keyDef, - }); - const collRef = shellCollectionRef(collName); const escapedIndexName = escapeDoubleQuotes(indexName); - options.push({ - label: l10n.t('Shell Command'), - detail: `${collRef}.getIndexes().find(i => i.name === "${escapedIndexName}")`, - value: `${collRef}.getIndexes().find(i => i.name === "${escapedIndexName}")`, - }); + options.push(opt(l10n.t('Key Definition'), keyDef)); + options.push( + opt(l10n.t('Shell Command'), `${collRef}.getIndexes().find(i => i.name === "${escapedIndexName}")`), + ); } return options; @@ -176,7 +133,7 @@ function getOptionsForNode(node: DatabaseItem | CollectionItem | IndexItem): { } export async function copyReference( - _context: IActionContext, + context: IActionContext, node: DatabaseItem | CollectionItem | IndexItem, ): Promise { if (!node) { @@ -185,32 +142,12 @@ export async function copyReference( const { title, options } = getOptionsForNode(node); - const picker = vscode.window.createQuickPick(); - picker.title = title; - picker.placeholder = l10n.t('Select a format to copy'); - picker.items = options; - picker.matchOnDetail = true; - - // Workaround: setting sortByLabel to false via the options object is not available - // in the createQuickPick API, but we can suppress persistence by not setting a value - // for the quickpick's value property, which avoids reordering. - - const result = await new Promise((resolve) => { - picker.onDidAccept(() => { - resolve(picker.selectedItems[0] as CopyReferenceOption | undefined); - picker.dispose(); - }); - picker.onDidHide(() => { - resolve(undefined); - picker.dispose(); - }); - picker.show(); + const picked = await context.ui.showQuickPick(options, { + placeHolder: title, + stepName: 'copyReference', + suppressPersistence: true, }); - if (!result) { - return; - } - - await vscode.env.clipboard.writeText(result.value); + await vscode.env.clipboard.writeText(picked.id); void vscode.window.showInformationMessage(l10n.t('Copied to clipboard')); }