Skip to content

Extension UI cleanup with multiple SQL extension#21101

Open
aasimkhan30 wants to merge 30 commits intomainfrom
aasim/feat/extensionCoord
Open

Extension UI cleanup with multiple SQL extension#21101
aasimkhan30 wants to merge 30 commits intomainfrom
aasim/feat/extensionCoord

Conversation

@aasimkhan30
Copy link
Contributor

@aasimkhan30 aasimkhan30 commented Feb 5, 2026

Description

Fixes: #19535

This PR implements a URI ownership coordination system between the MSSQL and PostgreSQL VS Code extensions. When both extensions are installed, they now coordinate to ensure only one extension shows UI elements (status bar, CodeLens) for a given SQL file at a time, based on which extension has an active connection to that file.

Problem

When both MSSQL and PostgreSQL extensions are installed, users experienced:

  • Duplicate status bar items (both extensions showing "Connect" or connection status)
  • Duplicate CodeLens prompts at the top of SQL files
  • Confusion about which extension is managing a particular SQL file
  • Ability to accidentally run commands against the wrong database type

Solution

Implemented a UriOwnershipCoordinator that:

  1. Exposes an API (uriOwnershipApi) for other extensions to query URI ownership
  2. Listens for ownership changes from coordinating extensions
  3. Hides UI elements when another extension owns the active file
  4. Shows informative messages when users try to use commands on files owned by another extension

Behavior

Scenario MSSQL UI PostgreSQL UI Screenshot
File not connected to either Show Shown image
File connected to MSSQL Shown Hidden image
File connected to PostgreSQL Hidden Shown image

User-Facing Messages

When a user tries to connect or run a query on a file connected to PostgreSQL through commands:

"This file is currently connected to PostgreSQL. Please disconnect from PostgreSQL before proceeding."

Backward Compatibility

  • ✅ Works normally if PostgreSQL extension is not installed
  • ✅ Works normally if PostgreSQL extension doesn't have uriOwnershipApi (older version)
  • ✅ No breaking changes to existing MSSQL functionality
  • ✅ Graceful degradation - both extensions work independently if coordination unavailable

Code Changes Checklist

  • New or updated unit tests added
  • All existing tests pass (npm run test)
  • Code follows contributing guidelines
  • Telemetry/logging updated if relevant
  • No regressions or UX breakage

Reviewers: Please read our reviewer guidelines

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements a URI ownership coordination system between the MSSQL and PostgreSQL VS Code extensions to prevent duplicate UI elements (status bar items, CodeLens, editor buttons) when both extensions are installed. The coordination is achieved through a shared API that allows extensions to query which extension owns a given file URI.

Changes:

  • Introduced UriOwnershipCoordinator class to manage coordination with other database extensions
  • Added command name suffixes "(MSSQL)" to disambiguate from PostgreSQL commands in the command palette
  • Implemented UI hiding logic in status bar, CodeLens, and editor menus based on URI ownership

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
extensions/mssql/src/uriOwnership.ts New coordinator class that manages URI ownership API and listens for ownership changes from other extensions
extensions/mssql/typings/vscode-mssql.d.ts Added UriOwnershipApi interface to the public API
extensions/mssql/src/extension.ts Instantiates and initializes the URI ownership coordinator
extensions/mssql/src/views/statusView.ts Hides status bar items when active file is owned by another extension
extensions/mssql/src/queryResult/sqlCodeLensProvider.ts Hides CodeLens when URI is owned by another extension
extensions/mssql/package.json Added context-based visibility conditions to editor menu commands
extensions/mssql/src/controllers/mainController.ts Added ownership checks to prevent command execution on files owned by other extensions
extensions/mssql/src/constants/locConstants.ts Added localized error message for files owned by other extensions
extensions/mssql/package.nls.json Updated command titles with "(MSSQL)" suffix
localization/xliff/vscode-mssql.xlf Updated localization resources
extensions/mssql/l10n/bundle.l10n.json Updated l10n bundle with new messages
Comments suppressed due to low confidence (2)

extensions/mssql/package.json:440

  • For defensive programming and consistency, consider adding the !mssql.hideUIElements condition to other MSSQL-specific commands in the editor/title menu (disconnect, changeConnection, changeDatabase, showEstimatedPlan, etc.), not just runQuery and connect. While the resource in mssql.connections check should prevent these buttons from showing when a file is owned by another extension, adding the explicit hideUIElements check would provide an extra layer of protection against potential race conditions during connection handoff between extensions.
            "editor/title": [
                {
                    "command": "mssql.runQuery",
                    "when": "editorLangId == sql && !isInDiffEditor && resourcePath not in mssql.runningQueries && !mssql.hideUIElements",
                    "group": "navigation@1"
                },
                {
                    "command": "mssql.cancelQuery",
                    "when": "editorLangId == sql && !isInDiffEditor && resourcePath in mssql.runningQueries",
                    "group": "navigation@2"
                },
                {
                    "command": "mssql.revealQueryResult",
                    "when": "editorLangId == sql && resource in mssql.connections && !isInDiffEditor",
                    "group": "navigation@2"
                },
                {
                    "command": "mssql.connect",
                    "when": "editorLangId == sql && !isInDiffEditor && resource not in mssql.connections && !mssql.hideUIElements",
                    "group": "navigation@3"
                },
                {
                    "command": "mssql.disconnect",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
                    "group": "navigation@3"
                },
                {
                    "command": "mssql.changeConnection",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
                    "group": "navigation@4"
                },
                {
                    "command": "mssql.changeDatabase",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
                    "group": "navigation@5"
                },
                {
                    "command": "mssql.showEstimatedPlan",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
                    "group": "navigation@10"
                },
                {
                    "command": "mssql.enableActualPlan",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && (resource not in mssql.executionPlan.urisWithActualPlanEnabled) && (resourcePath not in mssql.runningQueries)",
                    "group": "navigation@11"
                },
                {
                    "command": "mssql.disableActualPlan",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && (resource in mssql.executionPlan.urisWithActualPlanEnabled) && (resourcePath not in mssql.runningQueries)",
                    "group": "navigation@12"
                }

extensions/mssql/src/controllers/mainController.ts:241

  • The cmdChangeConnection command handler doesn't check for URI ownership like cmdConnect and cmdRunQuery do. While the editor menu buttons are hidden via package.json conditions, users can still invoke this command from the command palette. If a file is owned by another extension (e.g., PostgreSQL), users could potentially invoke "Change Connection (MSSQL)" from the command palette and get unexpected behavior. Consider adding the same ownership check here for consistency.
            this.registerCommand(Constants.cmdChangeConnection);
            this._event.on(Constants.cmdChangeConnection, () => {
                void this.runAndLogErrors(this.onNewConnection());
            });

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@github-actions
Copy link

github-actions bot commented Feb 5, 2026

PR Changes

Category Target Branch PR Branch Difference
vscode-mssql VSIX 6316 KB 6317 KB ⚪ 1 KB ( 0% )
sql-database-projects VSIX 7024 KB 7024 KB ⚪ 0 KB ( 0% )
data-workspace VSIX 535 KB 535 KB ⚪ 0 KB ( 0% )

@codecov-commenter
Copy link

codecov-commenter commented Feb 5, 2026

Codecov Report

❌ Patch coverage is 32.60870% with 279 lines in your changes missing coverage. Please review.
✅ Project coverage is 72.56%. Comparing base (2f3ea41) to head (6a70739).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
...ensions/mssql/src/uriOwnership/uriOwnershipCore.ts 25.44% 208 Missing ⚠️
extensions/mssql/src/views/statusView.ts 21.62% 29 Missing ⚠️
...sql/src/uriOwnership/uriOwnershipInitialization.ts 37.50% 20 Missing ⚠️
extensions/mssql/src/extension.ts 46.15% 7 Missing ⚠️
extensions/mssql/src/controllers/mainController.ts 14.28% 6 Missing ⚠️
...sions/mssql/src/queryResult/sqlCodeLensProvider.ts 53.84% 6 Missing ⚠️
...nsions/mssql/src/controllers/sqlDocumentService.ts 90.90% 3 Missing ⚠️

❌ Your patch status has failed because the patch coverage (32.60%) is below the target coverage (70.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main   #21101      +/-   ##
==========================================
- Coverage   72.73%   72.56%   -0.18%     
==========================================
  Files         321      323       +2     
  Lines       95281    95690     +409     
  Branches     5236     5249      +13     
==========================================
+ Hits        69304    69434     +130     
- Misses      25977    26256     +279     
Files with missing lines Coverage Δ
...nsions/mssql/src/controllers/sqlDocumentService.ts 90.85% <90.90%> (-0.06%) ⬇️
extensions/mssql/src/controllers/mainController.ts 30.28% <14.28%> (-0.04%) ⬇️
...sions/mssql/src/queryResult/sqlCodeLensProvider.ts 63.39% <53.84%> (-1.61%) ⬇️
extensions/mssql/src/extension.ts 21.45% <46.15%> (+1.45%) ⬆️
...sql/src/uriOwnership/uriOwnershipInitialization.ts 37.50% <37.50%> (ø)
extensions/mssql/src/views/statusView.ts 46.80% <21.62%> (-1.68%) ⬇️
...ensions/mssql/src/uriOwnership/uriOwnershipCore.ts 25.44% <25.44%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copilot AI review requested due to automatic review settings February 5, 2026 09:33
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 11 out of 12 changed files in this pull request and generated 8 comments.

Comments suppressed due to low confidence (1)

extensions/mssql/package.json:440

  • Several editor/title menu items that should be hidden when the URI is owned by another extension are missing the && !mssql.hideUIElements condition:
  • mssql.disconnect (line 413)
  • mssql.changeConnection (line 418)
  • mssql.changeDatabase (line 423)
  • mssql.revealQueryResult (line 403)
  • mssql.cancelQuery (line 398)
  • mssql.showEstimatedPlan (line 428)
  • mssql.enableActualPlan (line 433)
  • mssql.disableActualPlan (line 438)

These commands all operate on the active editor and should not be visible when the file is owned by a coordinating extension like PostgreSQL, for consistency with the mssql.runQuery and mssql.connect commands that already have this condition.

                {
                    "command": "mssql.cancelQuery",
                    "when": "editorLangId == sql && !isInDiffEditor && resourcePath in mssql.runningQueries",
                    "group": "navigation@2"
                },
                {
                    "command": "mssql.revealQueryResult",
                    "when": "editorLangId == sql && resource in mssql.connections && !isInDiffEditor",
                    "group": "navigation@2"
                },
                {
                    "command": "mssql.connect",
                    "when": "editorLangId == sql && !isInDiffEditor && resource not in mssql.connections && !mssql.hideUIElements",
                    "group": "navigation@3"
                },
                {
                    "command": "mssql.disconnect",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
                    "group": "navigation@3"
                },
                {
                    "command": "mssql.changeConnection",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
                    "group": "navigation@4"
                },
                {
                    "command": "mssql.changeDatabase",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
                    "group": "navigation@5"
                },
                {
                    "command": "mssql.showEstimatedPlan",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
                    "group": "navigation@10"
                },
                {
                    "command": "mssql.enableActualPlan",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && (resource not in mssql.executionPlan.urisWithActualPlanEnabled) && (resourcePath not in mssql.runningQueries)",
                    "group": "navigation@11"
                },
                {
                    "command": "mssql.disableActualPlan",
                    "when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && (resource in mssql.executionPlan.urisWithActualPlanEnabled) && (resourcePath not in mssql.runningQueries)",
                    "group": "navigation@12"
                }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings February 5, 2026 09:50
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 13 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copilot AI review requested due to automatic review settings February 6, 2026 06:52
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 23 out of 27 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +296 to +310
private _refreshCoordinatingExtensions(): void {
const newExtensions = discoverCoordinatingExtensions(this._context.extension.id);

// Find newly added extensions
for (const extInfo of newExtensions) {
if (!this._coordinatingExtensionApis.has(extInfo.extensionId)) {
const extension = vscode.extensions.getExtension(extInfo.extensionId);
if (extension?.isActive) {
this._registerCoordinatingExtensionApi(extInfo.extensionId, extension.exports);
}
}
}

this._coordinatingExtensions = newExtensions;
}
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _refreshCoordinatingExtensions method does not handle the case where an extension is uninstalled or deactivated. When an extension is removed, it remains in _coordinatingExtensionApis Map and its event listener remains registered in context.subscriptions, potentially causing errors when the extension tries to call the API or when events fire. The method should check for removed extensions and clean up their entries from _coordinatingExtensionApis.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 6, 2026 07:08
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 24 out of 28 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Aasim Khan added 3 commits February 5, 2026 23:15
…oord

# Conflicts:
#	extensions/mssql/package.json
#	extensions/mssql/src/controllers/mainController.ts
#	extensions/mssql/src/extension.ts
#	extensions/mssql/src/queryResult/sqlCodeLensProvider.ts
Copilot AI review requested due to automatic review settings February 27, 2026 18:30
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 21 out of 25 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +6797 to +6803
<source xml:lang="en">Cancel Query (MSSQL)</source>
</trans-unit>
<trans-unit id="mssql.changeConnection">
<source xml:lang="en">Change Connection</source>
<source xml:lang="en">Change Connection (MSSQL)</source>
</trans-unit>
<trans-unit id="mssql.changeDatabase">
<source xml:lang="en">Change Database</source>
<source xml:lang="en">Change Database (MSSQL)</source>
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The localized translation files (de, es, fr, it, ja, ko, pt-BR, ru, zh-Hans, zh-Hant) have not been updated to reflect the command name changes. The source file vscode-mssql.xlf shows updated strings like "Execute Query (MSSQL)" but the German translation file vscode-mssql.de.xlf still has the old English source text "Cancel Query" without "(MSSQL)" suffix at line 8733.

According to the localization workflow memory, when adding or updating trans-units in vscode-mssql.xlf, they must be added/updated in all 10 localized files with target state="new" containing the English source text. This ensures translators know these strings need translation.

Copilot uses AI. Check for mistakes.
Comment on lines +391 to +433
"command": "mssql.cancelQuery",
"when": "editorLangId == sql && !isInDiffEditor && resourcePath in mssql.runningQueries",
"group": "navigation@2"
},
{
"command": "mssql.revealQueryResult",
"when": "editorLangId == sql && resource in mssql.connections && !isInDiffEditor",
"group": "navigation@2"
},
{
"command": "mssql.connect",
"when": "editorLangId == sql && !isInDiffEditor && resource not in mssql.connections && !mssql.hideUIElements",
"group": "navigation@3"
},
{
"command": "mssql.disconnect",
"when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
"group": "navigation@3"
},
{
"command": "mssql.changeConnection",
"when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
"group": "navigation@4"
},
{
"command": "mssql.changeDatabase",
"when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
"group": "navigation@5"
},
{
"command": "mssql.showEstimatedPlan",
"when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && resourcePath not in mssql.runningQueries",
"group": "navigation@10"
},
{
"command": "mssql.enableActualPlan",
"when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && (resource not in mssql.executionPlan.urisWithActualPlanEnabled) && (resourcePath not in mssql.runningQueries)",
"group": "navigation@11"
},
{
"command": "mssql.disableActualPlan",
"when": "editorLangId == sql && !isInDiffEditor && resource in mssql.connections && (resource in mssql.executionPlan.urisWithActualPlanEnabled) && (resourcePath not in mssql.runningQueries)",
"group": "navigation@12"
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several editor/title menu items are missing the !mssql.hideUIElements condition and will remain visible when a file is owned by a coordinating extension (e.g., PostgreSQL). These commands should be hidden to avoid user confusion:

  • Line 407: mssql.disconnect - shown when connected
  • Line 412: mssql.changeConnection - shown when connected
  • Line 417: mssql.changeDatabase - shown when connected
  • Line 422: mssql.showEstimatedPlan - shown when connected
  • Line 427: mssql.enableActualPlan - shown when connected
  • Line 432: mssql.disableActualPlan - shown when connected
  • Line 392: mssql.cancelQuery - shown when query is running
  • Line 397: mssql.revealQueryResult - shown when results exist

These should include && !mssql.hideUIElements in their "when" clause for consistency with mssql.runQuery (line 387) and mssql.connect (line 402).

Copilot uses AI. Check for mistakes.
Comment on lines +144 to +146
/** Optional localized default warning message factory */
fileOwnedByOtherExtensionMessage?: (owningExtensionDisplayName: string) => string;

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The README.md documents a fileOwnedByOtherExtensionMessage configuration option at line 145, but this property is not present in the actual UriOwnershipConfig interface defined in types.ts (lines 54-87). This is a documentation error - the option doesn't exist in the implementation.

The warning message is actually generated in isActiveEditorOwnedByOtherExtensionWithWarning() at uriOwnershipCoordinator.ts lines 215-217 using a default format, and can be customized by passing a warningMessage parameter to that method.

Suggested change
/** Optional localized default warning message factory */
fileOwnedByOtherExtensionMessage?: (owningExtensionDisplayName: string) => string;

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 27, 2026 19:39
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 14 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +241 to +242
const newExtensions = discoverCoordinatingExtensions(this._context.extension.id);

Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_refreshCoordinatingExtensions adds newly discovered APIs but never removes entries from _coordinatingExtensionApis for extensions that are no longer installed/disabled. That can leave stale ownership checks. Consider pruning the map to the currently discovered extension IDs during refresh.

Suggested change
const newExtensions = discoverCoordinatingExtensions(this._context.extension.id);
const newExtensions = discoverCoordinatingExtensions(this._context.extension.id);
const discoveredIds = new Set(newExtensions.map(ext => ext.extensionId));
// Remove APIs for extensions that are no longer discovered.
for (const existingId of this._coordinatingExtensionApis.keys()) {
if (!discoveredIds.has(existingId)) {
this._coordinatingExtensionApis.delete(existingId);
}
}

Copilot uses AI. Check for mistakes.
Comment on lines +8 to +10
// TODO: Move URI ownership core/coordinator logic to a shared package.
const PACKAGE_JSON_COMMON_FEATURES_KEY = "vscode-sql-common-features";
const SET_CONTEXT_COMMAND = "setContext";
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file duplicates the coordinator logic that was also added under packages/vscode-sql-common/src/uriOwnership. Maintaining two implementations will likely diverge and reintroduce coordination bugs. Consider importing and using the shared vscode-sql-common coordinator here (and removing this TODO), or remove the unused shared implementation from this PR.

Copilot uses AI. Check for mistakes.
): vscode.CodeLens[] | Thenable<vscode.CodeLens[]> {
// Defer to notebook-specific code lens provider for notebook cells
if (document.uri.scheme === "vscode-notebook-cell") {
if (document.uri.scheme === "vscode-notebook-cell" || uriOwnershipCoordinator?.isOwnedByCoordinatingExtension(document.uri)) {
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if condition is long enough that it’s unlikely to match the repo’s Prettier formatting; it should be wrapped to avoid lint/format failures and improve readability.

Suggested change
if (document.uri.scheme === "vscode-notebook-cell" || uriOwnershipCoordinator?.isOwnedByCoordinatingExtension(document.uri)) {
if (
document.uri.scheme === "vscode-notebook-cell" ||
uriOwnershipCoordinator?.isOwnedByCoordinatingExtension(document.uri)
) {

Copilot uses AI. Check for mistakes.
Comment on lines +308 to +312
editor.document.languageId === Constants.languageId &&
uriOwnershipCoordinator?.isOwnedByCoordinatingExtension(editor.document.uri)
) {
this._lastActiveConnectionInfo = undefined;
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new _lastActiveConnectionInfo clearing logic for coordinating-extension-owned SQL files should be covered by unit tests (there are already several onDidChangeActiveTextEditor cases in sqlDocumentService.test.ts). Add a case where the URI is reported as owned and assert _lastActiveConnectionInfo is cleared.

Copilot uses AI. Check for mistakes.
Comment on lines +249 to +252
// Don't auto-connect if this document is owned by a coordinating extension.
if (uriOwnershipCoordinator?.isOwnedByCoordinatingExtension(doc.uri)) {
return;
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

URI-ownership behavior added here (skipping MSSQL auto-connect when another extension owns the document) isn’t covered by the existing sqlDocumentService unit tests. Please add a test that stubs uriOwnershipCoordinator.isOwnedByCoordinatingExtension(...) to return true and asserts that connect(...) is not called for SQL docs.

Copilot uses AI. Check for mistakes.
Comment on lines 41 to 44
// Defer to notebook-specific code lens provider for notebook cells
if (document.uri.scheme === "vscode-notebook-cell") {
if (document.uri.scheme === "vscode-notebook-cell" || uriOwnershipCoordinator?.isOwnedByCoordinatingExtension(document.uri)) {
return [];
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

provideCodeLenses now suppresses CodeLens when a coordinating extension owns the URI, but the existing SqlCodeLensProvider unit tests don’t cover this branch. Please add a test that stubs uriOwnershipCoordinator.isOwnedByCoordinatingExtension(...) to return true and asserts that no lenses are produced.

Copilot uses AI. Check for mistakes.
Comment on lines 6796 to +6800
<trans-unit id="mssql.cancelQuery">
<source xml:lang="en">Cancel Query</source>
<source xml:lang="en">Cancel Query (MSSQL)</source>
</trans-unit>
<trans-unit id="mssql.changeConnection">
<source xml:lang="en">Change Connection</source>
<source xml:lang="en">Change Connection (MSSQL)</source>
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These vscode-mssql.xlf source strings were updated, but the corresponding localized XLF files (de/es/fr/it/ja/ko/pt-BR/ru/zh-Hans/zh-Hant) still contain the old <source> text for the same trans-unit IDs. Please update those localized files as well (typically marking targets as state="new") to keep localization in sync.

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +63
// Listen for ownership changes from coordinating extensions
if (uriOwnershipCoordinator) {
uriOwnershipCoordinator.onCoordinatingOwnershipChanged(() => {
const activeEditor = vscode.window.activeTextEditor;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The subscription returned by uriOwnershipCoordinator.onCoordinatingOwnershipChanged(...) isn’t disposed, so it will leak listeners across extension reloads/tests. Store the returned Disposable (e.g., in a field) and dispose it in StatusView.dispose() (or register it in a subscription bag).

Copilot uses AI. Check for mistakes.
Comment on lines +60 to +63
// Listen for ownership changes from coordinating extensions
if (uriOwnershipCoordinator) {
uriOwnershipCoordinator.onCoordinatingOwnershipChanged(() => {
const activeEditor = vscode.window.activeTextEditor;
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

New URI-ownership behavior in StatusView (reacting to onCoordinatingOwnershipChanged) isn’t covered by the existing unit tests for this file. Please add a unit test that stubs uriOwnershipCoordinator and verifies the status bar is hidden/shown appropriately when ownership flips.

Copilot uses AI. Check for mistakes.
Copilot AI review requested due to automatic review settings February 27, 2026 21:47
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 12 out of 14 changed files in this pull request and generated 5 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +240 to +253
private _refreshCoordinatingExtensions(): void {
const newExtensions = discoverCoordinatingExtensions(this._context.extension.id);

for (const extInfo of newExtensions) {
if (!this._coordinatingExtensionApis.has(extInfo.extensionId)) {
const extension = vscode.extensions.getExtension(extInfo.extensionId);
if (extension?.isActive) {
this._registerCoordinatingExtensionApi(extInfo.extensionId, extension.exports);
}
}
}

this._coordinatingExtensions = newExtensions;
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In _refreshCoordinatingExtensions(), when a newly discovered extension is found that hasn't been registered yet, only active extensions are registered (line 246 checks extension?.isActive). However, in the initial discovery path (_discoverAndRegisterExtensions()), inactive extensions are explicitly activated via extension.activate(). The refresh path skips inactive extensions silently, so a newly installed extension that is not yet active at refresh time will not be registered even after it later activates. Consider calling extension.activate() in the refresh path too, consistent with _discoverAndRegisterExtensions().

Copilot uses AI. Check for mistakes.
{
"command": "mssql.deployNewDatabase",
"when": "view == objectExplorer",
"when": "config.mssql.enableRichExperiences && view == objectExplorer",
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The mssql.deployNewDatabase menu entry was changed to add config.mssql.enableRichExperiences && to its when clause. However, mssql.enableRichExperiences is not defined as a configuration property in package.json and per the codebase conventions this setting was retired (the correct gate for private preview features is mssql.enableExperimentalFeatures). Since enableRichExperiences is undefined, this condition will always evaluate to false, effectively hiding the "Deploy New Database" button from the Object Explorer for all users. This appears to be an unintended regression introduced by this PR. The condition should either be removed (reverting to "when": "view == objectExplorer") or replaced with config.mssql.enableExperimentalFeatures if it's intended to be feature-gated.

Suggested change
"when": "config.mssql.enableRichExperiences && view == objectExplorer",
"when": "view == objectExplorer",

Copilot uses AI. Check for mistakes.
Comment on lines +73 to +279
export class UriOwnershipCoordinator {
public readonly uriOwnershipApi: UriOwnershipApi;
public readonly onCoordinatingOwnershipChanged: vscode.Event<void>;

private readonly _context: vscode.ExtensionContext;
private readonly _hideUiContextKey: string;
private readonly _fileOwnedByOtherExtensionMessage?: (extensionName: string) => string;
private readonly _coordinatingExtensionApis: Map<string, UriOwnershipApi> = new Map();
private readonly _coordinatingOwnershipChangedEmitter = new vscode.EventEmitter<void>();
private readonly _uriOwnershipChangedEmitter = new vscode.EventEmitter<void>();

private _coordinatingExtensions: CoordinatingExtensionInfo[] = [];
private _ownsUri: ((uri: string) => boolean) | undefined;
private _releaseUri: ((uri: string) => void | Promise<void>) | undefined;
private _initialized = false;

constructor(context: vscode.ExtensionContext, config: UriOwnershipConfig) {
this._context = context;
this._hideUiContextKey = config.hideUiContextKey;
this._fileOwnedByOtherExtensionMessage = config.fileOwnedByOtherExtensionMessage;

this._context.subscriptions.push(this._coordinatingOwnershipChangedEmitter);
this._context.subscriptions.push(this._uriOwnershipChangedEmitter);

this.uriOwnershipApi = {
ownsUri: (uri: vscode.Uri): boolean => {
return this._ownsUri?.(uri.toString(true)) ?? false;
},
onDidChangeUriOwnership: this._uriOwnershipChangedEmitter.event,
};

this.onCoordinatingOwnershipChanged = this._coordinatingOwnershipChangedEmitter.event;

if (config.ownsUri && config.onDidChangeOwnership) {
this._initializeCallbacks({
ownsUri: config.ownsUri,
onDidChangeOwnership: config.onDidChangeOwnership,
releaseUri: config.releaseUri,
});
}

this._discoverAndRegisterExtensions();
this._registerActiveEditorListener();
this._registerExtensionChangeListener();
}

public initialize(config: UriOwnershipDeferredConfig): void {
if (this._initialized) {
return;
}
this._initializeCallbacks(config);
}

private _initializeCallbacks(config: UriOwnershipDeferredConfig): void {
if (this._initialized) {
return;
}

this._ownsUri = config.ownsUri;
this._releaseUri = config.releaseUri;
this._initialized = true;

this._context.subscriptions.push(
config.onDidChangeOwnership(() => {
this._uriOwnershipChangedEmitter.fire();
}),
);

this._updateUriOwnershipContext();
}

public getOwningCoordinatingExtension(uri: vscode.Uri): string | undefined {
for (const [extensionId, api] of this._coordinatingExtensionApis.entries()) {
if (api.ownsUri(uri)) {
return extensionId;
}
}
return undefined;
}

public isOwnedByCoordinatingExtension(uri: vscode.Uri): boolean {
return this.getOwningCoordinatingExtension(uri) !== undefined;
}

public isActiveEditorOwnedByOtherExtensionWithWarning(warningMessage?: string): boolean {
const activeUri = vscode.window.activeTextEditor?.document?.uri;
if (activeUri) {
const owningExtensionId = this.getOwningCoordinatingExtension(activeUri);
if (owningExtensionId) {
const extensionName = getExtensionDisplayName(
owningExtensionId,
this._coordinatingExtensions,
);
const message =
warningMessage ||
this._fileOwnedByOtherExtensionMessage?.(extensionName) ||
`This file is connected to ${extensionName}. Please use ${extensionName} commands for this file.`;
void vscode.window.showInformationMessage(message);
return true;
}
}
return false;
}

public getCoordinatingExtensions(): ReadonlyArray<CoordinatingExtensionInfo> {
return this._coordinatingExtensions;
}

private _discoverAndRegisterExtensions(): void {
this._coordinatingExtensions = discoverCoordinatingExtensions(this._context.extension.id);

for (const extInfo of this._coordinatingExtensions) {
const extension = vscode.extensions.getExtension(extInfo.extensionId);
if (!extension) {
continue;
}

if (!extension.isActive) {
extension.activate().then(
(exports) => {
this._registerCoordinatingExtensionApi(extInfo.extensionId, exports);
},
(err) => {
console.error(
`[${this._context.extension.id}] Error activating coordinating extension ${extInfo.extensionId}: ${err}`,
);
},
);
} else {
this._registerCoordinatingExtensionApi(extInfo.extensionId, extension.exports);
}
}
}

private _registerCoordinatingExtensionApi(extensionId: string, exports: unknown): void {
const api = (exports as { uriOwnershipApi?: UriOwnershipApi })?.uriOwnershipApi;
if (api) {
this._coordinatingExtensionApis.set(extensionId, api);

if (api.onDidChangeUriOwnership) {
this._context.subscriptions.push(
api.onDidChangeUriOwnership(() => {
this._updateUriOwnershipContext();
}),
);
}
}
}

private _registerActiveEditorListener(): void {
this._context.subscriptions.push(
vscode.window.onDidChangeActiveTextEditor(() => {
this._updateUriOwnershipContext();
}),
);

this._updateUriOwnershipContext();
}

private _registerExtensionChangeListener(): void {
this._context.subscriptions.push(
vscode.extensions.onDidChange(() => {
this._refreshCoordinatingExtensions();
}),
);
}

private _refreshCoordinatingExtensions(): void {
const newExtensions = discoverCoordinatingExtensions(this._context.extension.id);

for (const extInfo of newExtensions) {
if (!this._coordinatingExtensionApis.has(extInfo.extensionId)) {
const extension = vscode.extensions.getExtension(extInfo.extensionId);
if (extension?.isActive) {
this._registerCoordinatingExtensionApi(extInfo.extensionId, extension.exports);
}
}
}

this._coordinatingExtensions = newExtensions;
}

private _updateUriOwnershipContext(): void {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
void vscode.commands.executeCommand(SET_CONTEXT_COMMAND, this._hideUiContextKey, false);
return;
}

const uri = activeEditor.document.uri;
const uriString = uri.toString(true);
const isOwnedByOther = this.isOwnedByCoordinatingExtension(uri);
const isOwnedBySelf = this._ownsUri?.(uriString) ?? false;

if (isOwnedByOther && isOwnedBySelf && this._releaseUri) {
void Promise.resolve(this._releaseUri(uriString));
}

void vscode.commands.executeCommand(
SET_CONTEXT_COMMAND,
this._hideUiContextKey,
isOwnedByOther,
);

this._coordinatingOwnershipChangedEmitter.fire();
}
}
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The UriOwnershipCoordinator class in uriOwnershipCore.ts has no unit tests, despite the project having comprehensive unit test coverage for similar subsystems (e.g., sqlCodeLensProvider.test.ts, sqlDocumentService.test.ts, statusView.test.ts). Key behaviors that could benefit from test coverage include: isOwnedByCoordinatingExtension(), conflict resolution in _updateUriOwnershipContext() (the isOwnedByOther && isOwnedBySelf && _releaseUri branch), discoverCoordinatingExtensions() extension filtering, and initialize() being a no-op after first call.

Copilot uses AI. Check for mistakes.
Comment on lines +255 to +277
private _updateUriOwnershipContext(): void {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
void vscode.commands.executeCommand(SET_CONTEXT_COMMAND, this._hideUiContextKey, false);
return;
}

const uri = activeEditor.document.uri;
const uriString = uri.toString(true);
const isOwnedByOther = this.isOwnedByCoordinatingExtension(uri);
const isOwnedBySelf = this._ownsUri?.(uriString) ?? false;

if (isOwnedByOther && isOwnedBySelf && this._releaseUri) {
void Promise.resolve(this._releaseUri(uriString));
}

void vscode.commands.executeCommand(
SET_CONTEXT_COMMAND,
this._hideUiContextKey,
isOwnedByOther,
);

this._coordinatingOwnershipChangedEmitter.fire();
Copy link

Copilot AI Feb 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The _updateUriOwnershipContext() method unconditionally fires _coordinatingOwnershipChangedEmitter every time it is called — including on every active editor change (vscode.window.onDidChangeActiveTextEditor). This means onCoordinatingOwnershipChanged fires on every tab switch, triggering unnecessary work in all subscribers (StatusView's handler, SqlCodeLensProvider, etc.) even when the ownership state hasn't changed. The emitter should only be fired when the isOwnedByOther value actually changes, i.e. when transitioning from owned-by-other to not-owned-by-other or vice versa. This can be achieved by caching the previous value and only emitting when it changes.

Suggested change
private _updateUriOwnershipContext(): void {
const activeEditor = vscode.window.activeTextEditor;
if (!activeEditor) {
void vscode.commands.executeCommand(SET_CONTEXT_COMMAND, this._hideUiContextKey, false);
return;
}
const uri = activeEditor.document.uri;
const uriString = uri.toString(true);
const isOwnedByOther = this.isOwnedByCoordinatingExtension(uri);
const isOwnedBySelf = this._ownsUri?.(uriString) ?? false;
if (isOwnedByOther && isOwnedBySelf && this._releaseUri) {
void Promise.resolve(this._releaseUri(uriString));
}
void vscode.commands.executeCommand(
SET_CONTEXT_COMMAND,
this._hideUiContextKey,
isOwnedByOther,
);
this._coordinatingOwnershipChangedEmitter.fire();
private _lastIsOwnedByOther: boolean | undefined;
private _updateUriOwnershipContext(): void {
const activeEditor = vscode.window.activeTextEditor;
let isOwnedByOther = false;
if (!activeEditor) {
void vscode.commands.executeCommand(SET_CONTEXT_COMMAND, this._hideUiContextKey, false);
} else {
const uri = activeEditor.document.uri;
const uriString = uri.toString(true);
isOwnedByOther = this.isOwnedByCoordinatingExtension(uri);
const isOwnedBySelf = this._ownsUri?.(uriString) ?? false;
if (isOwnedByOther && isOwnedBySelf && this._releaseUri) {
void Promise.resolve(this._releaseUri(uriString));
}
void vscode.commands.executeCommand(
SET_CONTEXT_COMMAND,
this._hideUiContextKey,
isOwnedByOther,
);
}
if (this._lastIsOwnedByOther !== isOwnedByOther) {
this._lastIsOwnedByOther = isOwnedByOther;
this._coordinatingOwnershipChangedEmitter.fire();
}

Copilot uses AI. Check for mistakes.
@kburtram
Copy link
Member

@aasimkhan30 this might be a good PR to discuss in a design meeting. It's like to make sure I understand the details of what we're doing here. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Query editor shows duplicate icons when MSSQL and PostgreSQL extensions are both installed

5 participants