diff --git a/.vscode/launch.json b/.vscode/launch.json index 13720cc9e2..4ffdf78065 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -63,7 +63,7 @@ "outFiles": [ "${workspaceFolder}/out/**/*.js" ] - }, + }, { "type": "chrome", "request": "attach", @@ -95,7 +95,7 @@ "name": "Unit Tests", "protocol": "inspector", "program": "${workspaceFolder}/node_modules/mocha/bin/_mocha", - "runtimeExecutable": "${workspaceFolder}/.build/electron/sqlops.app/Contents/MacOS/Electron", + "runtimeExecutable": "${workspaceFolder}/.build/electron/SQL Operations Studio.app/Contents/MacOS/Electron", "windows": { "runtimeExecutable": "${workspaceFolder}/.build/electron/sqlops.exe" }, diff --git a/src/sql/parts/connection/common/connectionManagement.ts b/src/sql/parts/connection/common/connectionManagement.ts index 3e48b4a15b..fbe7242abe 100644 --- a/src/sql/parts/connection/common/connectionManagement.ts +++ b/src/sql/parts/connection/common/connectionManagement.ts @@ -220,6 +220,8 @@ export interface IConnectionManagementService { canChangeConnectionConfig(profile: ConnectionProfile, newGroupID: string): boolean; + getTabColorForUri(uri: string): string; + /** * Sends a notification that the language flavor for a given URI has changed. * For SQL, this would be the specific SQL implementation being used. diff --git a/src/sql/parts/connection/common/connectionManagementService.ts b/src/sql/parts/connection/common/connectionManagementService.ts index 25bd426aef..f55d8868dc 100644 --- a/src/sql/parts/connection/common/connectionManagementService.ts +++ b/src/sql/parts/connection/common/connectionManagementService.ts @@ -148,6 +148,11 @@ export class ConnectionManagementService implements IConnectionManagementService this.disposables.push(this._onAddConnectionProfile); this.disposables.push(this._onDeleteConnectionProfile); + + // Refresh editor titles when connections start/end/change to ensure tabs are colored correctly + this.onConnectionChanged(() => this.refreshEditorTitles()); + this.onConnect(() => this.refreshEditorTitles()); + this.onDisconnect(() => this.refreshEditorTitles()); } // Event Emitters @@ -1219,6 +1224,7 @@ export class ConnectionManagementService implements IConnectionManagementService public editGroup(group: ConnectionProfileGroup): Promise { return new Promise((resolve, reject) => { this._connectionStore.editGroup(group).then(groupId => { + this.refreshEditorTitles(); this._onAddConnectionProfile.fire(); resolve(null); }).catch(err => { @@ -1323,4 +1329,25 @@ export class ConnectionManagementService implements IConnectionManagementService } return Promise.reject('The given URI is not currently connected'); } + + public getTabColorForUri(uri: string): string { + if (!WorkbenchUtils.getSqlConfigValue(this._workspaceConfigurationService, 'enableTabColors')) { + return undefined; + } + let connectionProfile = this.getConnectionProfile(uri); + if (!connectionProfile) { + return undefined; + } + let matchingGroup = this._connectionStore.getGroupFromId(connectionProfile.groupId); + if (!matchingGroup) { + return undefined; + } + return matchingGroup.color; + } + + private refreshEditorTitles(): void { + if (this._editorGroupService instanceof EditorPart) { + this._editorGroupService.refreshEditorTitles(); + } + } } diff --git a/src/sql/parts/connection/common/connectionStore.ts b/src/sql/parts/connection/common/connectionStore.ts index c2c336fcdc..fa96c71847 100644 --- a/src/sql/parts/connection/common/connectionStore.ts +++ b/src/sql/parts/connection/common/connectionStore.ts @@ -19,7 +19,6 @@ import { ConfigurationEditingService } from 'vs/workbench/services/configuration import { IWorkspaceConfigurationService } from 'vs/workbench/services/configuration/common/configuration'; import { ICapabilitiesService } from 'sql/services/capabilities/capabilitiesService'; import * as data from 'data'; -import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; const MAX_CONNECTIONS_DEFAULT = 25; @@ -490,6 +489,11 @@ export class ConnectionStore { return result; } + public getGroupFromId(groupId: string): IConnectionProfileGroup { + let groups = this._connectionConfig.getAllGroups(); + return groups.find(group => group.id === groupId); + } + private getMaxRecentConnectionsCount(): number { let config = this._workspaceConfigurationService.getConfiguration(Constants.sqlConfigSectionName); diff --git a/src/sql/parts/dashboard/dashboardInput.ts b/src/sql/parts/dashboard/dashboardInput.ts index 34fecf193b..b4436cbbef 100644 --- a/src/sql/parts/dashboard/dashboardInput.ts +++ b/src/sql/parts/dashboard/dashboardInput.ts @@ -167,4 +167,8 @@ export class DashboardInput extends EditorInput { && profile1.authenticationType === profile2.authenticationType && profile1.groupFullName === profile2.groupFullName; } + + public get tabColor(): string { + return this._connectionService.getTabColorForUri(this.uri); + } } diff --git a/src/sql/parts/editData/common/editDataInput.ts b/src/sql/parts/editData/common/editDataInput.ts index 418e53e775..73595a4df9 100644 --- a/src/sql/parts/editData/common/editDataInput.ts +++ b/src/sql/parts/editData/common/editDataInput.ts @@ -180,4 +180,8 @@ export class EditDataInput extends EditorInput implements IConnectableInput { super.close(); }); } + + public get tabColor(): string { + return this._connectionManagementService.getTabColorForUri(this.uri); + } } diff --git a/src/sql/parts/query/common/query.contribution.ts b/src/sql/parts/query/common/query.contribution.ts index 515c12ab71..1af3298433 100644 --- a/src/sql/parts/query/common/query.contribution.ts +++ b/src/sql/parts/query/common/query.contribution.ts @@ -240,6 +240,11 @@ let registryProperties = { 'description': localize('sql.showBatchTime', '[Optional] Should execution time be shown for individual batches'), 'default': false }, + 'sql.enableTabColors': { + 'type': 'boolean', + 'description': localize('sql.enableTabColors', 'True to color tabs based on the server group of their active connection, false otherwise'), + 'default': true + }, 'mssql.intelliSense.enableIntelliSense': { 'type': 'boolean', 'default': true, diff --git a/src/sql/parts/query/common/queryInput.ts b/src/sql/parts/query/common/queryInput.ts index 332641d8f0..b0275df8be 100644 --- a/src/sql/parts/query/common/queryInput.ts +++ b/src/sql/parts/query/common/queryInput.ts @@ -252,4 +252,11 @@ export class QueryInput extends EditorInput implements IEncodingSupport, IConnec this._currentEventCallbacks = dispose(this._currentEventCallbacks); this._currentEventCallbacks = callbacks; } + + /** + * Get the color that should be displayed + */ + public get tabColor(): string { + return this._connectionManagementService.getTabColorForUri(this.uri); + } } \ No newline at end of file diff --git a/src/sqltest/parts/connection/connectionManagementService.test.ts b/src/sqltest/parts/connection/connectionManagementService.test.ts index fb566f0c73..963fd3e371 100644 --- a/src/sqltest/parts/connection/connectionManagementService.test.ts +++ b/src/sqltest/parts/connection/connectionManagementService.test.ts @@ -32,6 +32,7 @@ import { WorkspaceConfigurationTestService } from 'sqltest/stubs/workspaceConfig import * as assert from 'assert'; import * as TypeMoq from 'typemoq'; +import { IConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup'; suite('SQL ConnectionManagementService tests', () => { @@ -210,7 +211,7 @@ suite('SQL ConnectionManagementService tests', () => { let connectionToUse = connection ? connection : connectionProfile; return new Promise((resolve, reject) => { let id = connectionToUse.getOptionsKey(); - let defaultUri = 'connection://' + (id ? id : connection.serverName + ':' + connection.databaseName); + let defaultUri = 'connection://' + (id ? id : connectionToUse.serverName + ':' + connectionToUse.databaseName); connectionManagementService.onConnectionRequestSent(() => { let info: data.ConnectionInfoSummary = { connectionId: error ? undefined : 'id', @@ -290,7 +291,7 @@ suite('SQL ConnectionManagementService tests', () => { }).catch(err => { done(err); }); - }); + }, err => done(err)); }); test('connect should save profile given options with saveProfile set to true', done => { @@ -764,4 +765,29 @@ suite('SQL ConnectionManagementService tests', () => { } }, err => done(err)); }); + + test('getTabColorForUri returns undefined when there is no connection for the given URI', () => { + let connectionManagementService = createConnectionManagementService(); + let color = connectionManagementService.getTabColorForUri('invalidUri'); + assert.equal(color, undefined); + }); + + test('getTabColorForUri returns the group color corresponding to the connection for a URI', done => { + // Set up the connection store to give back a group for the expected connection profile + configResult['enableTabColors'] = true; + let expectedColor = 'red'; + connectionStore.setup(x => x.getGroupFromId(connectionProfile.groupId)).returns(() => { + color: expectedColor + }); + let uri = 'testUri'; + connect(uri).then(() => { + try { + let tabColor = connectionManagementService.getTabColorForUri(uri); + assert.equal(tabColor, expectedColor); + done(); + } catch (e) { + done(e); + } + }, err => done(err)); + }); }); \ No newline at end of file diff --git a/src/sqltest/parts/connection/connectionStore.test.ts b/src/sqltest/parts/connection/connectionStore.test.ts index 202739b5c0..2be21c4876 100644 --- a/src/sqltest/parts/connection/connectionStore.test.ts +++ b/src/sqltest/parts/connection/connectionStore.test.ts @@ -18,7 +18,7 @@ import { CapabilitiesService } from 'sql/services/capabilities/capabilitiesServi import * as data from 'data'; import { ConnectionProfile } from 'sql/parts/connection/common/connectionProfile'; import { Emitter } from 'vs/base/common/event'; -import { IConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup'; +import { ConnectionProfileGroup, IConnectionProfileGroup } from 'sql/parts/connection/common/connectionProfileGroup'; suite('SQL ConnectionStore tests', () => { let defaultNamedProfile: IConnectionProfile; @@ -93,7 +93,7 @@ suite('SQL ConnectionStore tests', () => { getInstalled: () => { return Promise.resolve([]); } - } + }; capabilitiesService = TypeMoq.Mock.ofType(CapabilitiesService, TypeMoq.MockBehavior.Loose, extensionManagementServiceMock, {}); let capabilities: data.DataProtocolServerCapabilities[] = []; @@ -462,4 +462,33 @@ suite('SQL ConnectionStore tests', () => { currentList = connectionStore.getConnectionsFromMemento(mementoKey); assert.equal(currentList.length, 3, 'Adding same connection with group /'); }); + + test('getGroupFromId returns undefined when there is no group with the given ID', () => { + let connectionStore = new ConnectionStore(storageServiceMock.object, context.object, undefined, workspaceConfigurationServiceMock.object, + credentialStore.object, capabilitiesService.object, connectionConfig.object); + let group = connectionStore.getGroupFromId('invalidId'); + assert.equal(group, undefined, 'Returned group was not undefined when there was no group with the given ID'); + }); + + test('getGroupFromId returns the group that has the given ID', () => { + // Set up the server groups with an additional group that contains a child group + let groups: IConnectionProfileGroup[] = connectionConfig.object.getAllGroups(); + let parentGroupId = 'parentGroup'; + let childGroupId = 'childGroup'; + let parentGroup = new ConnectionProfileGroup(parentGroupId, undefined, parentGroupId, '', ''); + let childGroup = new ConnectionProfileGroup(childGroupId, parentGroup, childGroupId, '', ''); + groups.push(parentGroup, childGroup); + let newConnectionConfig = TypeMoq.Mock.ofType(ConnectionConfig); + newConnectionConfig.setup(x => x.getAllGroups()).returns(() => groups); + let connectionStore = new ConnectionStore(storageServiceMock.object, context.object, undefined, workspaceConfigurationServiceMock.object, + credentialStore.object, capabilitiesService.object, newConnectionConfig.object); + + // If I look up the parent group using its ID, then I get back the correct group + let actualGroup = connectionStore.getGroupFromId(parentGroupId); + assert.equal(actualGroup.id, parentGroupId, 'Did not get the parent group when looking it up with its ID'); + + // If I look up the child group using its ID, then I get back the correct group + actualGroup = connectionStore.getGroupFromId(childGroupId); + assert.equal(actualGroup.id, childGroupId, 'Did not get the child group when looking it up with its ID'); + }); }); \ No newline at end of file diff --git a/src/sqltest/stubs/connectionManagementService.test.ts b/src/sqltest/stubs/connectionManagementService.test.ts index beb0552b49..c01a0b170d 100644 --- a/src/sqltest/stubs/connectionManagementService.test.ts +++ b/src/sqltest/stubs/connectionManagementService.test.ts @@ -237,4 +237,8 @@ export class TestConnectionManagementService implements IConnectionManagementSer rebuildIntelliSenseCache(uri: string): Thenable { return undefined; } + + getTabColorForUri(uri: string): string { + return undefined; + } } \ No newline at end of file diff --git a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts index 4eb31174ae..cb4b1a872a 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupsControl.ts @@ -87,6 +87,8 @@ export interface IEditorGroupsControl { getRatio(): number[]; + // {{SQL CARBON EDIT}} -- Allow editor titles to be refreshed to support tab coloring + refreshTitles(): void; dispose(): void; } @@ -2126,6 +2128,14 @@ export class EditorGroupsControl extends Themable implements IEditorGroupsContro } } + // {{SQL CARBON EDIT}} -- Allow editor titles to be refreshed to support tab coloring + public refreshTitles(): void { + POSITIONS.forEach(position => { + let titleControl = this.getTitleAreaControl(position); + titleControl.refresh(); + }); + } + public dispose(): void { super.dispose(); diff --git a/src/vs/workbench/browser/parts/editor/editorPart.ts b/src/vs/workbench/browser/parts/editor/editorPart.ts index 5db53645a1..4781edede3 100644 --- a/src/vs/workbench/browser/parts/editor/editorPart.ts +++ b/src/vs/workbench/browser/parts/editor/editorPart.ts @@ -1359,6 +1359,11 @@ export class EditorPart extends Part implements IEditorPart, IEditorGroupService return sizes; } + // {{SQL CARBON EDIT}} -- Allow editor titles to be refreshed to support tab coloring + public refreshEditorTitles(): void { + this.editorGroupsControl.refreshTitles(); + } + public shutdown(): void { // Persist UI State diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 3ab075bd15..c1fe826813 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -39,12 +39,15 @@ import { ScrollbarVisibility } from 'vs/base/common/scrollable'; import { extractResources } from 'vs/base/browser/dnd'; import { getOrSet } from 'vs/base/common/map'; import { ServiceCollection } from 'vs/platform/instantiation/common/serviceCollection'; -import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector } from 'vs/platform/theme/common/themeService'; +import { IThemeService, registerThemingParticipant, ITheme, ICssStyleCollector, HIGH_CONTRAST } from 'vs/platform/theme/common/themeService'; import { TAB_INACTIVE_BACKGROUND, TAB_ACTIVE_BACKGROUND, TAB_ACTIVE_FOREGROUND, TAB_INACTIVE_FOREGROUND, TAB_BORDER, EDITOR_DRAG_AND_DROP_BACKGROUND, TAB_UNFOCUSED_ACTIVE_FOREGROUND, TAB_UNFOCUSED_INACTIVE_FOREGROUND, TAB_UNFOCUSED_ACTIVE_BORDER, TAB_ACTIVE_BORDER } from 'vs/workbench/common/theme'; import { activeContrastBorder, contrastBorder } from 'vs/platform/theme/common/colorRegistry'; import { IFileService } from 'vs/platform/files/common/files'; import { IWorkspacesService } from 'vs/platform/workspaces/common/workspaces'; +// {{SQL CARBON EDIT}} -- Display the editor's tab color +import { Color } from 'vs/base/common/color'; + interface IEditorInputLabel { name: string; description?: string; @@ -330,6 +333,20 @@ export class TabsTitleControl extends TitleControl { } else { DOM.removeClass(tabContainer, 'dirty'); } + + // {{SQL CARBON EDIT}} -- Display the editor's tab color + let sqlEditor = editor as any; + if (sqlEditor.tabColor && this.themeService.getTheme().type !== HIGH_CONTRAST) { + tabContainer.style.borderTopColor = sqlEditor.tabColor; + tabContainer.style.borderTopWidth = isTabActive ? '2px' : '1px'; + let backgroundColor = Color.Format.CSS.parseHex(sqlEditor.tabColor); + if (backgroundColor) { + tabContainer.style.backgroundColor = backgroundColor.transparent(isTabActive ? 0.3 : 0.2).toString(); + } + } else { + tabContainer.style.borderTopColor = ''; + tabContainer.style.borderTopWidth = ''; + } } });