Skip to content

Commit 6fe4b90

Browse files
authored
Adding code lens suggestion for sql connection. (#19461)
* Add SqlCodeLensProvider for SQL connection management and code lens features * Add localization for SQL Server connection messages * Update connection label in QueryEditor to use 'MSSQL' instead of 'SQL Server' * Update connection label to use 'MSSQL' instead of 'SQL Server' in localization files * remove console statements * removing document arg * Add CodeLens suggestion for active SQL connection display and update related functionality * Remove unused connection name localization and add CodeLens suggestion for active SQL connection visibility * adding command to change connection and fix the display strings for the commands. * Refactor connection display functions to improve clarity and consistency in SQL CodeLens * Refactor SqlCodeLensProvider to streamline CodeLens item creation and improve readability
1 parent 7f9aa54 commit 6fe4b90

File tree

11 files changed

+202
-28
lines changed

11 files changed

+202
-28
lines changed

localization/l10n/bundle.l10n.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1271,6 +1271,7 @@
12711271
]
12721272
},
12731273
"An unknown error occurred. Please try again.": "An unknown error occurred. Please try again.",
1274+
"$(plug) Connect to MSSQL": "$(plug) Connect to MSSQL",
12741275
"Azure sign in failed.": "Azure sign in failed.",
12751276
"Select subscriptions": "Select subscriptions",
12761277
"Error loading Azure subscriptions.": "Error loading Azure subscriptions.",

localization/xliff/vscode-mssql.xlf

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
<trans-unit id="++CODE++fd10453f63e5c663d4e640ea9cbe1adcc832b6fed0454e62686773ac486ce64d">
55
<source xml:lang="en"> is required.</source>
66
</trans-unit>
7+
<trans-unit id="++CODE++18b63db64aa0aff55a9f4784ad62abdc2b70cedb95b5e89d5551996cc9bd25ac">
8+
<source xml:lang="en">$(plug) Connect to MSSQL</source>
9+
</trans-unit>
710
<trans-unit id="++CODE++0d7668d337e375d8ccfc1a69ca8f6e22a0b0c850a78c4770b0c4aa3b0daca630">
811
<source xml:lang="en">&lt;default&gt;</source>
912
</trans-unit>
@@ -2620,6 +2623,9 @@
26202623
<trans-unit id="mssql.format.placeSelectStatementReferencesOnNewLine">
26212624
<source xml:lang="en">Should references to objects in a select statements be split into separate lines? E.g. for &apos;SELECT C1, C2 FROM T1&apos; both C1 and C2 will be on separate lines</source>
26222625
</trans-unit>
2626+
<trans-unit id="mssql.query.showActiveConnectionAsCodeLensSuggestion">
2627+
<source xml:lang="en">Show the active SQL connection details as a CodeLens suggestion at the top of the editor for quick visibility.</source>
2628+
</trans-unit>
26232629
<trans-unit id="mssql.walkthroughs.nextSteps.sortAndFilterQueryResults.title">
26242630
<source xml:lang="en">Sort and Filter Query Results</source>
26252631
</trans-unit>

package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1790,6 +1790,11 @@
17901790
"default": false,
17911791
"description": "%mssql.query.alwaysEncryptedParameterization%"
17921792
},
1793+
"mssql.query.showActiveConnectionAsCodeLensSuggestion": {
1794+
"type": "boolean",
1795+
"default": true,
1796+
"description": "%mssql.query.showActiveConnectionAsCodeLensSuggestion%"
1797+
},
17931798
"mssql.ignorePlatformWarning": {
17941799
"type": "boolean",
17951800
"description": "%mssql.ignorePlatformWarning%",

package.nls.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@
155155
"mssql.query.ansiPadding": "Enable SET ANSI_PADDING",
156156
"mssql.query.ansiWarnings": "Enable SET ANSI_WARNINGS",
157157
"mssql.query.ansiNulls": "Enable SET ANSI_NULLS",
158+
"mssql.query.showActiveConnectionAsCodeLensSuggestion": "Show the active SQL connection details as a CodeLens suggestion at the top of the editor for quick visibility.",
158159
"mssql.query.alwaysEncryptedParameterization": "Enable Parameterization for Always Encrypted",
159160
"mssql.ignorePlatformWarning": "[Optional] Do not show unsupported platform warnings",
160161
"mssql.Configuration": "MSSQL configuration",

src/constants/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,8 @@ export const configInMemoryDataProcessingThreshold = "resultsGrid.inMemoryDataPr
205205
export const configAutoDisableNonTSqlLanguageService = "mssql.autoDisableNonTSqlLanguageService";
206206
export const copilotDebugLogging = "mssql.copilotDebugLogging";
207207
export const configSelectedAzureSubscriptions = "mssql.selectedAzureSubscriptions";
208+
export const configShowActiveConnectionAsCodeLensSuggestion =
209+
"mssql.query.showActiveConnectionAsCodeLensSuggestion";
208210

209211
// ToolsService Constants
210212
export const serviceInstallingTo = "Installing SQL tools service to";

src/constants/locConstants.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -995,3 +995,7 @@ export class MssqlChatAgent {
995995
};
996996
public static unknownErrorOccurred = l10n.t("An unknown error occurred. Please try again.");
997997
}
998+
999+
export class QueryEditor {
1000+
public static codeLensConnect = l10n.t("$(plug) Connect to MSSQL");
1001+
}

src/controllers/mainController.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ import { ConnectionNode } from "../objectExplorer/nodes/connectionNode";
6767
import { CopilotService } from "../services/copilotService";
6868
import * as Prompts from "../chat/prompts";
6969
import { CreateSessionResult } from "../objectExplorer/objectExplorerService";
70+
import { SqlCodeLensProvider } from "../queryResult/sqlCodeLensProvider";
7071

7172
/**
7273
* The main controller class that initializes the extension
@@ -276,6 +277,14 @@ export default class MainController implements vscode.Disposable {
276277
this._event.on(Constants.cmdDisableActualPlan, () => {
277278
this.onToggleActualPlan(false);
278279
});
280+
281+
this._context.subscriptions.push(
282+
vscode.languages.registerCodeLensProvider(
283+
{ language: "sql" },
284+
new SqlCodeLensProvider(this._connectionMgr),
285+
),
286+
);
287+
279288
this.initializeObjectExplorer();
280289

281290
this.registerCommandWithArgs(Constants.cmdConnectObjectExplorerProfile);

src/models/connectionInfo.ts

Lines changed: 22 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import * as Constants from "../constants/constants";
88
import * as LocalizedConstants from "../constants/locConstants";
99
import { EncryptOptions } from "../models/interfaces";
1010
import * as Interfaces from "./interfaces";
11-
import * as Utils from "./utils";
1211

1312
/**
1413
* Sets sensible defaults for key connection properties, especially
@@ -138,31 +137,34 @@ export function getPicklistDetails(connCreds: IConnectionInfo): string {
138137
* @param conn connection
139138
* @returns display string that can be used in status view or other locations
140139
*/
141-
export function getConnectionDisplayString(creds: IConnectionInfo): string {
142-
// Update the connection text
143-
let text: string = creds.server;
144-
if (creds.database !== "") {
145-
text = appendIfNotEmpty(text, creds.database);
146-
} else {
147-
text = appendIfNotEmpty(text, LocalizedConstants.defaultDatabaseLabel);
148-
}
149-
let user: string = getUserNameOrDomainLogin(creds);
150-
text = appendIfNotEmpty(text, user);
140+
export function getConnectionDisplayString(creds: IConnectionInfo, trim: boolean = false): string {
141+
const server = generateServerDisplayName(creds);
142+
const database = generateDatabaseDisplayName(creds);
143+
const user = getUserNameOrDomainLogin(creds);
144+
145+
let result = user ? `${server} : ${database} : ${user}` : `${server} : ${database}`;
151146

152-
// Limit the maximum length of displayed text
153-
if (text && text.length > Constants.maxDisplayedStatusTextLength) {
154-
text = text.substr(0, Constants.maxDisplayedStatusTextLength);
155-
text += " \u2026"; // Ellipsis character (...)
147+
if (trim && result.length > Constants.maxDisplayedStatusTextLength) {
148+
result = result.slice(0, Constants.maxDisplayedStatusTextLength) + " \u2026"; // add ellipsis
156149
}
157150

158-
return text;
151+
return result;
159152
}
160153

161-
function appendIfNotEmpty(connectionText: string, value: string): string {
162-
if (Utils.isNotEmpty(value)) {
163-
connectionText += ` : ${value}`;
154+
export function generateServerDisplayName(creds: IConnectionInfo): string {
155+
return creds.server;
156+
}
157+
158+
export function generateDatabaseDisplayName(
159+
creds: IConnectionInfo,
160+
includeDatabaseIcon: boolean = true,
161+
): string {
162+
const databaseName = creds.database || LocalizedConstants.defaultDatabaseLabel;
163+
if (includeDatabaseIcon) {
164+
return `$(database) ${databaseName}`;
165+
} else {
166+
return databaseName;
164167
}
165-
return connectionText;
166168
}
167169

168170
/**
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
import * as vscode from "vscode";
7+
import * as Constants from "../constants/constants";
8+
import ConnectionManager from "../controllers/connectionManager";
9+
import { QueryEditor } from "../constants/locConstants";
10+
import { generateDatabaseDisplayName, generateServerDisplayName } from "../models/connectionInfo";
11+
12+
export class SqlCodeLensProvider implements vscode.CodeLensProvider {
13+
constructor(private _connectionManager: ConnectionManager) {}
14+
15+
public provideCodeLenses(
16+
document: vscode.TextDocument,
17+
token: vscode.CancellationToken,
18+
): vscode.CodeLens[] | Thenable<vscode.CodeLens[]> {
19+
const shouldShowActiveConnection = vscode.workspace
20+
.getConfiguration()
21+
.get<boolean>(Constants.configShowActiveConnectionAsCodeLensSuggestion);
22+
if (!shouldShowActiveConnection) {
23+
return [];
24+
}
25+
const connection = this._connectionManager.getConnectionInfo(document.uri.toString());
26+
27+
const items: vscode.CodeLens[] = [
28+
new vscode.CodeLens(new vscode.Range(0, 0, 0, 0), {
29+
title: connection
30+
? generateServerDisplayName(connection.credentials)
31+
: QueryEditor.codeLensConnect,
32+
command: Constants.cmdConnect,
33+
}),
34+
];
35+
if (connection) {
36+
items.push(
37+
new vscode.CodeLens(new vscode.Range(0, 0, 0, 0), {
38+
title: generateDatabaseDisplayName(connection.credentials),
39+
command: Constants.cmdChooseDatabase,
40+
}),
41+
);
42+
}
43+
return items;
44+
}
45+
46+
public resolveCodeLens?(
47+
codeLens: vscode.CodeLens,
48+
token: vscode.CancellationToken,
49+
): vscode.CodeLens | Thenable<vscode.CodeLens> {
50+
return undefined;
51+
}
52+
}

src/views/statusView.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ export default class StatusView implements vscode.Disposable {
186186
): void {
187187
let bar = this.getStatusBar(fileUri);
188188
bar.statusConnection.command = Constants.cmdChooseDatabase;
189-
bar.statusConnection.text = `$(check) ${ConnInfo.getConnectionDisplayString(connCreds)}`;
189+
bar.statusConnection.text = `$(check) ${ConnInfo.getConnectionDisplayString(connCreds, true)}`;
190190
bar.statusConnection.tooltip = ConnInfo.getTooltip(connCreds, serverInfo);
191191
this.showStatusBarItem(fileUri, bar.statusConnection);
192192
this.sqlCmdModeChanged(fileUri, false);

test/unit/connectionInfo.test.ts

Lines changed: 99 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import { expect } from "chai";
7-
import { getConnectionDisplayName } from "../../src/models/connectionInfo";
7+
import * as ConnectionInfo from "../../src/models/connectionInfo";
88
import * as LocalizedConstants from "../../src/constants/locConstants";
99
import * as Constants from "../../src/constants/constants";
1010
import { IConnectionInfo } from "vscode-mssql";
1111
import { IConnectionProfile } from "../../src/models/interfaces";
12+
import * as sinon from "sinon";
1213

1314
suite("connectionInfo", () => {
1415
suite("getConnectionDisplayName", () => {
@@ -20,7 +21,7 @@ suite("connectionInfo", () => {
2021
user: "testUser",
2122
} as IConnectionInfo;
2223

23-
const result = getConnectionDisplayName(connection);
24+
const result = ConnectionInfo.getConnectionDisplayName(connection);
2425
expect(result).to.equal("testServer, testDatabase (testUser)");
2526
});
2627

@@ -31,13 +32,13 @@ suite("connectionInfo", () => {
3132
user: "testUser",
3233
} as IConnectionInfo;
3334

34-
let result = getConnectionDisplayName(connection);
35+
let result = ConnectionInfo.getConnectionDisplayName(connection);
3536
expect(result).to.equal(
3637
`testServer, ${LocalizedConstants.defaultDatabaseLabel} (testUser)`,
3738
);
3839

3940
connection.database = "";
40-
result = getConnectionDisplayName(connection);
41+
result = ConnectionInfo.getConnectionDisplayName(connection);
4142
expect(result).to.equal(
4243
`testServer, ${LocalizedConstants.defaultDatabaseLabel} (testUser)`,
4344
);
@@ -51,7 +52,7 @@ suite("connectionInfo", () => {
5152
5253
} as IConnectionInfo;
5354

54-
const result = getConnectionDisplayName(connection);
55+
const result = ConnectionInfo.getConnectionDisplayName(connection);
5556
expect(result).to.equal("testServer, testDatabase ([email protected])");
5657
});
5758

@@ -62,7 +63,7 @@ suite("connectionInfo", () => {
6263
authenticationType: "OtherAuthType",
6364
} as IConnectionInfo;
6465

65-
const result = getConnectionDisplayName(connection);
66+
const result = ConnectionInfo.getConnectionDisplayName(connection);
6667
expect(result).to.equal("testServer, testDatabase (OtherAuthType)");
6768
});
6869

@@ -75,8 +76,99 @@ suite("connectionInfo", () => {
7576
profileName: "Test Profile Name",
7677
} as IConnectionProfile;
7778

78-
const result = getConnectionDisplayName(connection);
79+
const result = ConnectionInfo.getConnectionDisplayName(connection);
7980
expect(result).to.equal("Test Profile Name");
8081
});
8182
});
83+
84+
suite("getConnectionDisplayString", () => {
85+
// Setup common test variables
86+
const mockServer = "test-server";
87+
const mockDatabase = "test-database";
88+
const mockUser = "test-user";
89+
90+
let sandbox: sinon.SinonSandbox;
91+
let getUserNameStub: sinon.SinonStub;
92+
93+
setup(() => {
94+
// Create a sandbox for isolated sinon mocks
95+
sandbox = sinon.createSandbox();
96+
97+
// Stub the getUserNameOrDomainLogin function
98+
getUserNameStub = sandbox.stub(ConnectionInfo, "getUserNameOrDomainLogin");
99+
100+
// Set default constants values
101+
sandbox.stub(LocalizedConstants, "defaultDatabaseLabel").value("default-db");
102+
sandbox.stub(Constants, "maxDisplayedStatusTextLength").value(30);
103+
});
104+
105+
teardown(() => {
106+
// Restore all stubbed methods after each test
107+
sandbox.restore();
108+
});
109+
110+
test("returns formatted string with server, database and user", () => {
111+
// Arrange
112+
const creds: IConnectionInfo = {
113+
server: mockServer,
114+
database: mockDatabase,
115+
user: mockUser,
116+
} as IConnectionInfo;
117+
getUserNameStub.returns(mockUser);
118+
119+
// Act
120+
const result = ConnectionInfo.getConnectionDisplayString(creds);
121+
122+
// Assert
123+
expect(result).to.equal(`${mockServer} : $(database) ${mockDatabase} : ${mockUser}`);
124+
});
125+
126+
test("returns formatted string with server and database when user is not present", () => {
127+
// Arrange
128+
const creds: IConnectionInfo = {
129+
server: mockServer,
130+
database: mockDatabase,
131+
} as IConnectionInfo;
132+
getUserNameStub.returns("");
133+
134+
// Act
135+
const result = ConnectionInfo.getConnectionDisplayString(creds);
136+
137+
// Assert
138+
expect(result).to.equal(`${mockServer} : $(database) ${mockDatabase}`);
139+
});
140+
141+
test("uses default database label when database is not provided", () => {
142+
// Arrange
143+
const creds: IConnectionInfo = {
144+
server: mockServer,
145+
user: mockUser,
146+
} as IConnectionInfo;
147+
getUserNameStub.returns(mockUser);
148+
149+
// Act
150+
const result = ConnectionInfo.getConnectionDisplayString(creds);
151+
152+
// Assert
153+
expect(result).to.equal(`${mockServer} : $(database) default-db : ${mockUser}`);
154+
});
155+
156+
test("trims result when trim is true and result is longer than max length", () => {
157+
// Arrange
158+
const longServer = "very-long-server-name-that-exceeds-the-maximum-length";
159+
const creds: IConnectionInfo = {
160+
server: longServer,
161+
database: mockDatabase,
162+
} as IConnectionInfo;
163+
getUserNameStub.returns(mockUser);
164+
165+
// Act
166+
const result = ConnectionInfo.getConnectionDisplayString(creds, true);
167+
168+
// Assert
169+
const expectedText = `${longServer} : $(database) ${mockDatabase} : ${mockUser}`;
170+
const trimmedExpected = expectedText.slice(0, 30) + " \u2026";
171+
expect(result).to.equal(trimmedExpected);
172+
});
173+
});
82174
});

0 commit comments

Comments
 (0)