Path: blob/main/src/vs/workbench/api/test/browser/extHostEditorTabs.test.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import type * as vscode from 'vscode';6import assert from 'assert';7import { URI } from '../../../../base/common/uri.js';8import { mock } from '../../../../base/test/common/mock.js';9import { IEditorTabDto, IEditorTabGroupDto, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TextInputDto } from '../../common/extHost.protocol.js';10import { ExtHostEditorTabs } from '../../common/extHostEditorTabs.js';11import { SingleProxyRPCProtocol } from '../common/testRPCProtocol.js';12import { TextMergeTabInput, TextTabInput } from '../../common/extHostTypes.js';13import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../base/test/common/utils.js';1415suite('ExtHostEditorTabs', function () {1617const defaultTabDto: IEditorTabDto = {18id: 'uniquestring',19input: { kind: TabInputKind.TextInput, uri: URI.parse('file://abc/def.txt') },20isActive: true,21isDirty: true,22isPinned: true,23isPreview: false,24label: 'label1',25};2627function createTabDto(dto?: Partial<IEditorTabDto>): IEditorTabDto {28return { ...defaultTabDto, ...dto };29}3031const store = ensureNoDisposablesAreLeakedInTestSuite();3233test('Ensure empty model throws when accessing active group', function () {34const extHostEditorTabs = new ExtHostEditorTabs(35SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {36// override/implement $moveTab or $closeTab37})38);3940assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 0);41// Active group should never be undefined (there is always an active group). Ensure accessing it undefined throws.42// TODO @lramos15 Add a throw on the main side when a model is sent without an active group43assert.throws(() => extHostEditorTabs.tabGroups.activeTabGroup);44});4546test('single tab', function () {4748const extHostEditorTabs = new ExtHostEditorTabs(49SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {50// override/implement $moveTab or $closeTab51})52);5354const tab: IEditorTabDto = createTabDto({55id: 'uniquestring',56isActive: true,57isDirty: true,58isPinned: true,59label: 'label1',60});6162extHostEditorTabs.$acceptEditorTabModel([{63isActive: true,64viewColumn: 0,65groupId: 12,66tabs: [tab]67}]);68assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);69const [first] = extHostEditorTabs.tabGroups.all;70assert.ok(first.activeTab);71assert.strictEqual(first.tabs.indexOf(first.activeTab), 0);7273{74extHostEditorTabs.$acceptEditorTabModel([{75isActive: true,76viewColumn: 0,77groupId: 12,78tabs: [tab]79}]);80assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);81const [first] = extHostEditorTabs.tabGroups.all;82assert.ok(first.activeTab);83assert.strictEqual(first.tabs.indexOf(first.activeTab), 0);84}85});8687test('Empty tab group', function () {88const extHostEditorTabs = new ExtHostEditorTabs(89SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {90// override/implement $moveTab or $closeTab91})92);9394extHostEditorTabs.$acceptEditorTabModel([{95isActive: true,96viewColumn: 0,97groupId: 12,98tabs: []99}]);100assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);101const [first] = extHostEditorTabs.tabGroups.all;102assert.strictEqual(first.activeTab, undefined);103assert.strictEqual(first.tabs.length, 0);104});105106test('Ensure tabGroup change events fires', function () {107const extHostEditorTabs = new ExtHostEditorTabs(108SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {109// override/implement $moveTab or $closeTab110})111);112113let count = 0;114store.add(extHostEditorTabs.tabGroups.onDidChangeTabGroups(() => count++));115116assert.strictEqual(count, 0);117118extHostEditorTabs.$acceptEditorTabModel([{119isActive: true,120viewColumn: 0,121groupId: 12,122tabs: []123}]);124assert.ok(extHostEditorTabs.tabGroups.activeTabGroup);125const activeTabGroup: vscode.TabGroup = extHostEditorTabs.tabGroups.activeTabGroup;126assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);127assert.strictEqual(activeTabGroup.tabs.length, 0);128assert.strictEqual(count, 1);129});130131test('Check TabGroupChangeEvent properties', function () {132const extHostEditorTabs = new ExtHostEditorTabs(133SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {134// override/implement $moveTab or $closeTab135})136);137138const group1Data: IEditorTabGroupDto = {139isActive: true,140viewColumn: 0,141groupId: 12,142tabs: []143};144const group2Data: IEditorTabGroupDto = { ...group1Data, groupId: 13 };145146const events: vscode.TabGroupChangeEvent[] = [];147store.add(extHostEditorTabs.tabGroups.onDidChangeTabGroups(e => events.push(e)));148// OPEN149extHostEditorTabs.$acceptEditorTabModel([group1Data]);150assert.deepStrictEqual(events, [{151changed: [],152closed: [],153opened: [extHostEditorTabs.tabGroups.activeTabGroup]154}]);155156// OPEN, CHANGE157events.length = 0;158extHostEditorTabs.$acceptEditorTabModel([{ ...group1Data, isActive: false }, group2Data]);159assert.deepStrictEqual(events, [{160changed: [extHostEditorTabs.tabGroups.all[0]],161closed: [],162opened: [extHostEditorTabs.tabGroups.all[1]]163}]);164165// CHANGE166events.length = 0;167extHostEditorTabs.$acceptEditorTabModel([group1Data, { ...group2Data, isActive: false }]);168assert.deepStrictEqual(events, [{169changed: extHostEditorTabs.tabGroups.all,170closed: [],171opened: []172}]);173174// CLOSE, CHANGE175events.length = 0;176const oldActiveGroup = extHostEditorTabs.tabGroups.activeTabGroup;177extHostEditorTabs.$acceptEditorTabModel([group2Data]);178assert.deepStrictEqual(events, [{179changed: extHostEditorTabs.tabGroups.all,180closed: [oldActiveGroup],181opened: []182}]);183});184185test('Ensure reference equality for activeTab and activeGroup', function () {186const extHostEditorTabs = new ExtHostEditorTabs(187SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {188// override/implement $moveTab or $closeTab189})190);191const tab = createTabDto({192id: 'uniquestring',193isActive: true,194isDirty: true,195isPinned: true,196label: 'label1',197editorId: 'default',198});199200extHostEditorTabs.$acceptEditorTabModel([{201isActive: true,202viewColumn: 0,203groupId: 12,204tabs: [tab]205}]);206assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);207const [first] = extHostEditorTabs.tabGroups.all;208assert.ok(first.activeTab);209assert.strictEqual(first.tabs.indexOf(first.activeTab), 0);210assert.strictEqual(first.activeTab, first.tabs[0]);211assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup, first);212});213214test('TextMergeTabInput surfaces in the UI', function () {215216const extHostEditorTabs = new ExtHostEditorTabs(217SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {218// override/implement $moveTab or $closeTab219})220);221222const tab: IEditorTabDto = createTabDto({223input: {224kind: TabInputKind.TextMergeInput,225base: URI.from({ scheme: 'test', path: 'base' }),226input1: URI.from({ scheme: 'test', path: 'input1' }),227input2: URI.from({ scheme: 'test', path: 'input2' }),228result: URI.from({ scheme: 'test', path: 'result' }),229}230});231232extHostEditorTabs.$acceptEditorTabModel([{233isActive: true,234viewColumn: 0,235groupId: 12,236tabs: [tab]237}]);238assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);239const [first] = extHostEditorTabs.tabGroups.all;240assert.ok(first.activeTab);241assert.strictEqual(first.tabs.indexOf(first.activeTab), 0);242assert.ok(first.activeTab.input instanceof TextMergeTabInput);243});244245test('Ensure reference stability', function () {246247const extHostEditorTabs = new ExtHostEditorTabs(248SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {249// override/implement $moveTab or $closeTab250})251);252const tabDto = createTabDto();253254// single dirty tab255256extHostEditorTabs.$acceptEditorTabModel([{257isActive: true,258viewColumn: 0,259groupId: 12,260tabs: [tabDto]261}]);262let all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat();263assert.strictEqual(all.length, 1);264const apiTab1 = all[0];265assert.ok(apiTab1.input instanceof TextTabInput);266assert.strictEqual(tabDto.input.kind, TabInputKind.TextInput);267const dtoResource = (tabDto.input as TextInputDto).uri;268assert.strictEqual(apiTab1.input.uri.toString(), URI.revive(dtoResource).toString());269assert.strictEqual(apiTab1.isDirty, true);270271272// NOT DIRTY anymore273274const tabDto2: IEditorTabDto = { ...tabDto, isDirty: false };275// Accept a simple update276extHostEditorTabs.$acceptTabOperation({277kind: TabModelOperationKind.TAB_UPDATE,278index: 0,279tabDto: tabDto2,280groupId: 12281});282283all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat();284assert.strictEqual(all.length, 1);285const apiTab2 = all[0];286assert.ok(apiTab1.input instanceof TextTabInput);287assert.strictEqual(apiTab1.input.uri.toString(), URI.revive(dtoResource).toString());288assert.strictEqual(apiTab2.isDirty, false);289290assert.strictEqual(apiTab1 === apiTab2, true);291});292293test('Tab.isActive working', function () {294295const extHostEditorTabs = new ExtHostEditorTabs(296SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {297// override/implement $moveTab or $closeTab298})299);300const tabDtoAAA = createTabDto({301id: 'AAA',302isActive: true,303isDirty: true,304isPinned: true,305label: 'label1',306input: { kind: TabInputKind.TextInput, uri: URI.parse('file://abc/AAA.txt') },307editorId: 'default'308});309310const tabDtoBBB = createTabDto({311id: 'BBB',312isActive: false,313isDirty: true,314isPinned: true,315label: 'label1',316input: { kind: TabInputKind.TextInput, uri: URI.parse('file://abc/BBB.txt') },317editorId: 'default'318});319320// single dirty tab321322extHostEditorTabs.$acceptEditorTabModel([{323isActive: true,324viewColumn: 0,325groupId: 12,326tabs: [tabDtoAAA, tabDtoBBB]327}]);328329const all = extHostEditorTabs.tabGroups.all.map(group => group.tabs).flat();330assert.strictEqual(all.length, 2);331332const activeTab1 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab;333assert.ok(activeTab1?.input instanceof TextTabInput);334assert.strictEqual(tabDtoAAA.input.kind, TabInputKind.TextInput);335const dtoAAAResource = (tabDtoAAA.input as TextInputDto).uri;336assert.strictEqual(activeTab1?.input?.uri.toString(), URI.revive(dtoAAAResource)?.toString());337assert.strictEqual(activeTab1?.isActive, true);338339extHostEditorTabs.$acceptTabOperation({340groupId: 12,341index: 1,342kind: TabModelOperationKind.TAB_UPDATE,343tabDto: { ...tabDtoBBB, isActive: true } /// BBB is now active344});345346const activeTab2 = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab;347assert.ok(activeTab2?.input instanceof TextTabInput);348assert.strictEqual(tabDtoBBB.input.kind, TabInputKind.TextInput);349const dtoBBBResource = (tabDtoBBB.input as TextInputDto).uri;350assert.strictEqual(activeTab2?.input?.uri.toString(), URI.revive(dtoBBBResource)?.toString());351assert.strictEqual(activeTab2?.isActive, true);352assert.strictEqual(activeTab1?.isActive, false);353});354355test('vscode.window.tagGroups is immutable', function () {356357const extHostEditorTabs = new ExtHostEditorTabs(358SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {359// override/implement $moveTab or $closeTab360})361);362363assert.throws(() => {364// @ts-expect-error write to readonly prop365extHostEditorTabs.tabGroups.activeTabGroup = undefined;366});367assert.throws(() => {368// @ts-expect-error write to readonly prop369extHostEditorTabs.tabGroups.all.length = 0;370});371assert.throws(() => {372// @ts-expect-error write to readonly prop373extHostEditorTabs.tabGroups.onDidChangeActiveTabGroup = undefined;374});375assert.throws(() => {376// @ts-expect-error write to readonly prop377extHostEditorTabs.tabGroups.onDidChangeTabGroups = undefined;378});379});380381test('Ensure close is called with all tab ids', function () {382const closedTabIds: string[][] = [];383const extHostEditorTabs = new ExtHostEditorTabs(384SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {385// override/implement $moveTab or $closeTab386override async $closeTab(tabIds: string[], preserveFocus?: boolean) {387closedTabIds.push(tabIds);388return true;389}390})391);392const tab: IEditorTabDto = createTabDto({393id: 'uniquestring',394isActive: true,395isDirty: true,396isPinned: true,397label: 'label1',398editorId: 'default'399});400401extHostEditorTabs.$acceptEditorTabModel([{402isActive: true,403viewColumn: 0,404groupId: 12,405tabs: [tab]406}]);407assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);408const activeTab = extHostEditorTabs.tabGroups.activeTabGroup?.activeTab;409assert.ok(activeTab);410extHostEditorTabs.tabGroups.close(activeTab, false);411assert.strictEqual(closedTabIds.length, 1);412assert.deepStrictEqual(closedTabIds[0], ['uniquestring']);413// Close with array414extHostEditorTabs.tabGroups.close([activeTab], false);415assert.strictEqual(closedTabIds.length, 2);416assert.deepStrictEqual(closedTabIds[1], ['uniquestring']);417});418419test('Update tab only sends tab change event', async function () {420const closedTabIds: string[][] = [];421const extHostEditorTabs = new ExtHostEditorTabs(422SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {423// override/implement $moveTab or $closeTab424override async $closeTab(tabIds: string[], preserveFocus?: boolean) {425closedTabIds.push(tabIds);426return true;427}428})429);430const tabDto: IEditorTabDto = createTabDto({431id: 'uniquestring',432isActive: true,433isDirty: true,434isPinned: true,435label: 'label1',436editorId: 'default'437});438439extHostEditorTabs.$acceptEditorTabModel([{440isActive: true,441viewColumn: 0,442groupId: 12,443tabs: [tabDto]444}]);445446assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);447assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1);448449const tab = extHostEditorTabs.tabGroups.all[0].tabs[0];450451452const p = new Promise<vscode.TabChangeEvent>(resolve => store.add(extHostEditorTabs.tabGroups.onDidChangeTabs(resolve)));453454extHostEditorTabs.$acceptTabOperation({455groupId: 12,456index: 0,457kind: TabModelOperationKind.TAB_UPDATE,458tabDto: { ...tabDto, label: 'NEW LABEL' }459});460461const changedTab = (await p).changed[0];462463assert.ok(tab === changedTab);464assert.strictEqual(changedTab.label, 'NEW LABEL');465466});467468test('Active tab', function () {469470const extHostEditorTabs = new ExtHostEditorTabs(471SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {472// override/implement $moveTab or $closeTab473})474);475476const tab1: IEditorTabDto = createTabDto({477id: 'uniquestring',478isActive: true,479isDirty: true,480isPinned: true,481label: 'label1',482});483484const tab2: IEditorTabDto = createTabDto({485isActive: false,486id: 'uniquestring2',487});488489const tab3: IEditorTabDto = createTabDto({490isActive: false,491id: 'uniquestring3',492});493494extHostEditorTabs.$acceptEditorTabModel([{495isActive: true,496viewColumn: 0,497groupId: 12,498tabs: [tab1, tab2, tab3]499}]);500501assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);502assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3);503504// Active tab is correct505assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, extHostEditorTabs.tabGroups.activeTabGroup?.tabs[0]);506507// Switching active tab works508tab1.isActive = false;509tab2.isActive = true;510extHostEditorTabs.$acceptTabOperation({511groupId: 12,512index: 0,513kind: TabModelOperationKind.TAB_UPDATE,514tabDto: tab1515});516extHostEditorTabs.$acceptTabOperation({517groupId: 12,518index: 1,519kind: TabModelOperationKind.TAB_UPDATE,520tabDto: tab2521});522assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, extHostEditorTabs.tabGroups.activeTabGroup?.tabs[1]);523524//Closing tabs out works525tab3.isActive = true;526extHostEditorTabs.$acceptEditorTabModel([{527isActive: true,528viewColumn: 0,529groupId: 12,530tabs: [tab3]531}]);532assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);533assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1);534assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, extHostEditorTabs.tabGroups.activeTabGroup?.tabs[0]);535536// Closing out all tabs returns undefine active tab537extHostEditorTabs.$acceptEditorTabModel([{538isActive: true,539viewColumn: 0,540groupId: 12,541tabs: []542}]);543assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);544assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 0);545assert.strictEqual(extHostEditorTabs.tabGroups.activeTabGroup?.activeTab, undefined);546});547548test('Tab operations patches open and close correctly', function () {549const extHostEditorTabs = new ExtHostEditorTabs(550SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {551// override/implement $moveTab or $closeTab552})553);554555const tab1: IEditorTabDto = createTabDto({556id: 'uniquestring',557isActive: true,558label: 'label1',559});560561const tab2: IEditorTabDto = createTabDto({562isActive: false,563id: 'uniquestring2',564label: 'label2',565});566567const tab3: IEditorTabDto = createTabDto({568isActive: false,569id: 'uniquestring3',570label: 'label3',571});572573extHostEditorTabs.$acceptEditorTabModel([{574isActive: true,575viewColumn: 0,576groupId: 12,577tabs: [tab1, tab2, tab3]578}]);579580assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);581assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3);582583// Close tab 2584extHostEditorTabs.$acceptTabOperation({585groupId: 12,586index: 1,587kind: TabModelOperationKind.TAB_CLOSE,588tabDto: tab2589});590assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);591assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 2);592593// Close active tab and update tab 3 to be active594extHostEditorTabs.$acceptTabOperation({595groupId: 12,596index: 0,597kind: TabModelOperationKind.TAB_CLOSE,598tabDto: tab1599});600assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);601assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1);602tab3.isActive = true;603extHostEditorTabs.$acceptTabOperation({604groupId: 12,605index: 0,606kind: TabModelOperationKind.TAB_UPDATE,607tabDto: tab3608});609assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);610assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 1);611assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.activeTab?.label, 'label3');612613// Open tab 2 back614extHostEditorTabs.$acceptTabOperation({615groupId: 12,616index: 1,617kind: TabModelOperationKind.TAB_OPEN,618tabDto: tab2619});620assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);621assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 2);622assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[1]?.label, 'label2');623});624625test('Tab operations patches move correctly', function () {626const extHostEditorTabs = new ExtHostEditorTabs(627SingleProxyRPCProtocol(new class extends mock<MainThreadEditorTabsShape>() {628// override/implement $moveTab or $closeTab629})630);631632const tab1: IEditorTabDto = createTabDto({633id: 'uniquestring',634isActive: true,635label: 'label1',636});637638const tab2: IEditorTabDto = createTabDto({639isActive: false,640id: 'uniquestring2',641label: 'label2',642});643644const tab3: IEditorTabDto = createTabDto({645isActive: false,646id: 'uniquestring3',647label: 'label3',648});649650extHostEditorTabs.$acceptEditorTabModel([{651isActive: true,652viewColumn: 0,653groupId: 12,654tabs: [tab1, tab2, tab3]655}]);656657assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);658assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3);659660// Move tab 2 to index 0661extHostEditorTabs.$acceptTabOperation({662groupId: 12,663index: 0,664oldIndex: 1,665kind: TabModelOperationKind.TAB_MOVE,666tabDto: tab2667});668assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);669assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3);670assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[0]?.label, 'label2');671672// Move tab 3 to index 1673extHostEditorTabs.$acceptTabOperation({674groupId: 12,675index: 1,676oldIndex: 2,677kind: TabModelOperationKind.TAB_MOVE,678tabDto: tab3679});680assert.strictEqual(extHostEditorTabs.tabGroups.all.length, 1);681assert.strictEqual(extHostEditorTabs.tabGroups.all.map(g => g.tabs).flat().length, 3);682assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[1]?.label, 'label3');683assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[0]?.label, 'label2');684assert.strictEqual(extHostEditorTabs.tabGroups.all[0]?.tabs[2]?.label, 'label1');685});686});687688689