@theia/plugin-ext 1.34.0 → 1.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/common/plugin-api-rpc.d.ts +1 -1
- package/lib/common/plugin-api-rpc.d.ts.map +1 -1
- package/lib/main/browser/main-context.d.ts.map +1 -1
- package/lib/main/browser/main-context.js +3 -0
- package/lib/main/browser/main-context.js.map +1 -1
- package/lib/main/browser/tabs/tabs-main.d.ts +33 -2
- package/lib/main/browser/tabs/tabs-main.d.ts.map +1 -1
- package/lib/main/browser/tabs/tabs-main.js +256 -6
- package/lib/main/browser/tabs/tabs-main.js.map +1 -1
- package/lib/plugin/tabs.d.ts.map +1 -1
- package/lib/plugin/tabs.js +10 -13
- package/lib/plugin/tabs.js.map +1 -1
- package/package.json +27 -26
- package/src/common/plugin-api-rpc.ts +1 -1
- package/src/main/browser/main-context.ts +4 -0
- package/src/main/browser/tabs/tabs-main.ts +287 -6
- package/src/plugin/tabs.ts +10 -14
|
@@ -15,16 +15,275 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { interfaces } from '@theia/core/shared/inversify';
|
|
18
|
-
|
|
19
|
-
import { TabsMain } from '../../../common/plugin-api-rpc';
|
|
18
|
+
import { ApplicationShell, PINNED_CLASS, Saveable, TabBar, Title, ViewContainer, Widget } from '@theia/core/lib/browser';
|
|
19
|
+
import { AnyInputDto, MAIN_RPC_CONTEXT, TabDto, TabGroupDto, TabInputKind, TabModelOperationKind, TabsExt, TabsMain } from '../../../common/plugin-api-rpc';
|
|
20
20
|
import { RPCProtocol } from '../../../common/rpc-protocol';
|
|
21
|
+
import { EditorPreviewWidget } from '@theia/editor-preview/lib/browser/editor-preview-widget';
|
|
22
|
+
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
|
|
23
|
+
import { MonacoDiffEditor } from '@theia/monaco/lib/browser/monaco-diff-editor';
|
|
24
|
+
import { toUriComponents } from '../hierarchy/hierarchy-types-converters';
|
|
25
|
+
import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget';
|
|
26
|
+
import { DisposableCollection } from '@theia/core';
|
|
27
|
+
|
|
28
|
+
interface TabInfo {
|
|
29
|
+
tab: TabDto;
|
|
30
|
+
tabIndex: number;
|
|
31
|
+
group: TabGroupDto;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export class TabsMainImpl implements TabsMain, Disposable {
|
|
35
|
+
|
|
36
|
+
private readonly proxy: TabsExt;
|
|
37
|
+
private tabGroupModel = new Map<TabBar<Widget>, TabGroupDto>();
|
|
38
|
+
private tabInfoLookup = new Map<Title<Widget>, TabInfo>();
|
|
39
|
+
|
|
40
|
+
private applicationShell: ApplicationShell;
|
|
41
|
+
|
|
42
|
+
private disposableTabBarListeners: DisposableCollection = new DisposableCollection();
|
|
43
|
+
private toDisposeOnDestroy: DisposableCollection = new DisposableCollection();
|
|
21
44
|
|
|
22
|
-
|
|
45
|
+
private groupIdCounter = 0;
|
|
46
|
+
private currentActiveGroup: TabGroupDto;
|
|
47
|
+
|
|
48
|
+
private tabGroupChanged: boolean = false;
|
|
23
49
|
|
|
24
50
|
constructor(
|
|
25
51
|
rpc: RPCProtocol,
|
|
26
52
|
container: interfaces.Container
|
|
27
|
-
) {
|
|
53
|
+
) {
|
|
54
|
+
this.proxy = rpc.getProxy(MAIN_RPC_CONTEXT.TABS_EXT);
|
|
55
|
+
|
|
56
|
+
this.applicationShell = container.get(ApplicationShell);
|
|
57
|
+
this.createTabsModel();
|
|
58
|
+
|
|
59
|
+
const tabBars = this.applicationShell.mainPanel.tabBars();
|
|
60
|
+
for (let tabBar; tabBar = tabBars.next();) {
|
|
61
|
+
this.attachListenersToTabBar(tabBar);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.toDisposeOnDestroy.push(
|
|
65
|
+
this.applicationShell.mainPanelRenderer.onDidCreateTabBar(tabBar => {
|
|
66
|
+
this.attachListenersToTabBar(tabBar);
|
|
67
|
+
this.onTabGroupCreated(tabBar);
|
|
68
|
+
})
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
this.connectToSignal(this.toDisposeOnDestroy, this.applicationShell.mainPanel.widgetAdded, (mainPanel, widget) => {
|
|
72
|
+
if (this.tabGroupChanged || this.tabGroupModel.size === 0) {
|
|
73
|
+
this.tabGroupChanged = false;
|
|
74
|
+
this.createTabsModel();
|
|
75
|
+
// tab Open event is done in backend
|
|
76
|
+
} else {
|
|
77
|
+
const tabBar = mainPanel.findTabBar(widget.title)!;
|
|
78
|
+
const oldTabInfo = this.tabInfoLookup.get(widget.title);
|
|
79
|
+
const group = this.tabGroupModel.get(tabBar);
|
|
80
|
+
if (group !== oldTabInfo?.group) {
|
|
81
|
+
if (oldTabInfo) {
|
|
82
|
+
this.onTabClosed(oldTabInfo, widget.title);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.onTabCreated(tabBar, { index: tabBar.titles.indexOf(widget.title), title: widget.title });
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.connectToSignal(this.toDisposeOnDestroy, this.applicationShell.mainPanel.widgetRemoved, (mainPanel, widget) => {
|
|
91
|
+
if (!(widget instanceof TabBar)) {
|
|
92
|
+
const tabInfo = this.getOrRebuildModel(this.tabInfoLookup, widget.title)!;
|
|
93
|
+
this.onTabClosed(tabInfo, widget.title);
|
|
94
|
+
if (this.tabGroupChanged) {
|
|
95
|
+
this.tabGroupChanged = false;
|
|
96
|
+
this.createTabsModel();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
protected createTabsModel(): void {
|
|
103
|
+
const newTabGroupModel = new Map<TabBar<Widget>, TabGroupDto>();
|
|
104
|
+
this.tabInfoLookup.clear();
|
|
105
|
+
this.disposableTabBarListeners.dispose();
|
|
106
|
+
this.applicationShell.mainAreaTabBars.forEach(tabBar => {
|
|
107
|
+
this.attachListenersToTabBar(tabBar);
|
|
108
|
+
const groupDto = this.createTabGroupDto(tabBar);
|
|
109
|
+
tabBar.titles.forEach((title, index) => this.tabInfoLookup.set(title, { group: groupDto, tab: groupDto.tabs[index], tabIndex: index }));
|
|
110
|
+
newTabGroupModel.set(tabBar, groupDto);
|
|
111
|
+
});
|
|
112
|
+
if (newTabGroupModel.size > 0 && Array.from(newTabGroupModel.values()).indexOf(this.currentActiveGroup) < 0) {
|
|
113
|
+
this.currentActiveGroup = this.tabInfoLookup.get(this.applicationShell.mainPanel.currentTitle!)?.group ?? newTabGroupModel.values().next().value;
|
|
114
|
+
this.currentActiveGroup.isActive = true;
|
|
115
|
+
}
|
|
116
|
+
this.tabGroupModel = newTabGroupModel;
|
|
117
|
+
this.proxy.$acceptEditorTabModel(Array.from(this.tabGroupModel.values()));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
protected createTabDto(tabTitle: Title<Widget>, groupId: number): TabDto {
|
|
121
|
+
const widget = tabTitle.owner;
|
|
122
|
+
return {
|
|
123
|
+
id: this.createTabId(tabTitle, groupId),
|
|
124
|
+
label: tabTitle.label,
|
|
125
|
+
input: this.evaluateTabDtoInput(widget),
|
|
126
|
+
isActive: tabTitle.owner.isVisible,
|
|
127
|
+
isPinned: tabTitle.className.includes(PINNED_CLASS),
|
|
128
|
+
isDirty: Saveable.isDirty(widget),
|
|
129
|
+
isPreview: widget instanceof EditorPreviewWidget && widget.isPreview
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
protected createTabId(tabTitle: Title<Widget>, groupId: number): string {
|
|
134
|
+
return `${groupId}~${tabTitle.owner.id}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
protected createTabGroupDto(tabBar: TabBar<Widget>): TabGroupDto {
|
|
138
|
+
const oldDto = this.tabGroupModel.get(tabBar);
|
|
139
|
+
const groupId = oldDto?.groupId ?? this.groupIdCounter++;
|
|
140
|
+
const tabs = tabBar.titles.map(title => this.createTabDto(title, groupId));
|
|
141
|
+
return {
|
|
142
|
+
groupId,
|
|
143
|
+
tabs,
|
|
144
|
+
isActive: false,
|
|
145
|
+
viewColumn: 1
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected attachListenersToTabBar(tabBar: TabBar<Widget> | undefined): void {
|
|
150
|
+
if (!tabBar) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
tabBar.titles.forEach(title => {
|
|
154
|
+
this.connectToSignal(this.disposableTabBarListeners, title.changed, this.onTabTitleChanged);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
this.connectToSignal(this.disposableTabBarListeners, tabBar.tabMoved, this.onTabMoved);
|
|
158
|
+
this.connectToSignal(this.disposableTabBarListeners, tabBar.disposed, this.onTabGroupClosed);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
protected evaluateTabDtoInput(widget: Widget): AnyInputDto {
|
|
162
|
+
if (widget instanceof EditorPreviewWidget) {
|
|
163
|
+
if (widget.editor instanceof MonacoDiffEditor) {
|
|
164
|
+
return {
|
|
165
|
+
kind: TabInputKind.TextDiffInput,
|
|
166
|
+
original: toUriComponents(widget.editor.originalModel.uri),
|
|
167
|
+
modified: toUriComponents(widget.editor.modifiedModel.uri)
|
|
168
|
+
};
|
|
169
|
+
} else {
|
|
170
|
+
return {
|
|
171
|
+
kind: TabInputKind.TextInput,
|
|
172
|
+
uri: toUriComponents(widget.editor.uri.toString())
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
// TODO notebook support when implemented
|
|
176
|
+
} else if (widget instanceof ViewContainer) {
|
|
177
|
+
return {
|
|
178
|
+
kind: TabInputKind.WebviewEditorInput,
|
|
179
|
+
viewType: widget.id
|
|
180
|
+
};
|
|
181
|
+
} else if (widget instanceof TerminalWidget) {
|
|
182
|
+
return {
|
|
183
|
+
kind: TabInputKind.TerminalEditorInput
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return { kind: TabInputKind.UnknownInput };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
protected connectToSignal<T>(disposableList: DisposableCollection, signal: { connect(listener: T, context: unknown): void, disconnect(listener: T): void }, listener: T): void {
|
|
191
|
+
signal.connect(listener, this);
|
|
192
|
+
disposableList.push(Disposable.create(() => signal.disconnect(listener)));
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
protected tabDtosEqual(a: TabDto, b: TabDto): boolean {
|
|
196
|
+
return a.isActive === b.isActive &&
|
|
197
|
+
a.isDirty === b.isDirty &&
|
|
198
|
+
a.isPinned === b.isPinned &&
|
|
199
|
+
a.isPreview === b.isPreview &&
|
|
200
|
+
a.id === b.id;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
protected getOrRebuildModel<T, R>(map: Map<T, R>, key: T): R {
|
|
204
|
+
// something broke so we rebuild the model
|
|
205
|
+
let item = map.get(key);
|
|
206
|
+
if (!item) {
|
|
207
|
+
this.createTabsModel();
|
|
208
|
+
item = map.get(key)!;
|
|
209
|
+
}
|
|
210
|
+
return item;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// #region event listeners
|
|
214
|
+
private onTabCreated(tabBar: TabBar<Widget>, args: TabBar.ITabActivateRequestedArgs<Widget>): void {
|
|
215
|
+
const group = this.getOrRebuildModel(this.tabGroupModel, tabBar);
|
|
216
|
+
this.connectToSignal(this.disposableTabBarListeners, args.title.changed, this.onTabTitleChanged);
|
|
217
|
+
const tabDto = this.createTabDto(args.title, group.groupId);
|
|
218
|
+
this.tabInfoLookup.set(args.title, { group, tab: tabDto, tabIndex: args.index });
|
|
219
|
+
group.tabs.splice(args.index, 0, tabDto);
|
|
220
|
+
this.proxy.$acceptTabOperation({
|
|
221
|
+
kind: TabModelOperationKind.TAB_OPEN,
|
|
222
|
+
index: args.index,
|
|
223
|
+
tabDto,
|
|
224
|
+
groupId: group.groupId
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private onTabTitleChanged(title: Title<Widget>): void {
|
|
229
|
+
const tabInfo = this.getOrRebuildModel(this.tabInfoLookup, title);
|
|
230
|
+
if (!tabInfo) {
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const oldTabDto = tabInfo.tab;
|
|
234
|
+
const newTabDto = this.createTabDto(title, tabInfo.group.groupId);
|
|
235
|
+
if (newTabDto.isActive && !tabInfo.group.isActive) {
|
|
236
|
+
tabInfo.group.isActive = true;
|
|
237
|
+
this.currentActiveGroup.isActive = false;
|
|
238
|
+
this.currentActiveGroup = tabInfo.group;
|
|
239
|
+
this.proxy.$acceptTabGroupUpdate(tabInfo.group);
|
|
240
|
+
}
|
|
241
|
+
if (!this.tabDtosEqual(oldTabDto, newTabDto)) {
|
|
242
|
+
tabInfo.group.tabs[tabInfo.tabIndex] = newTabDto;
|
|
243
|
+
tabInfo.tab = newTabDto;
|
|
244
|
+
this.proxy.$acceptTabOperation({
|
|
245
|
+
kind: TabModelOperationKind.TAB_UPDATE,
|
|
246
|
+
index: tabInfo.tabIndex,
|
|
247
|
+
tabDto: newTabDto,
|
|
248
|
+
groupId: tabInfo.group.groupId
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private onTabClosed(tabInfo: TabInfo, title: Title<Widget>): void {
|
|
254
|
+
tabInfo.group.tabs.splice(tabInfo.tabIndex, 1);
|
|
255
|
+
this.tabInfoLookup.delete(title);
|
|
256
|
+
this.proxy.$acceptTabOperation({
|
|
257
|
+
kind: TabModelOperationKind.TAB_CLOSE,
|
|
258
|
+
index: tabInfo.tabIndex,
|
|
259
|
+
tabDto: this.createTabDto(title, tabInfo.group.groupId),
|
|
260
|
+
groupId: tabInfo.group.groupId
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private onTabMoved(tabBar: TabBar<Widget>, args: TabBar.ITabMovedArgs<Widget>): void {
|
|
265
|
+
const tabInfo = this.getOrRebuildModel(this.tabInfoLookup, args.title)!;
|
|
266
|
+
tabInfo.tabIndex = args.toIndex;
|
|
267
|
+
const tabDto = this.createTabDto(args.title, tabInfo.group.groupId);
|
|
268
|
+
tabInfo.group.tabs.splice(args.fromIndex, 1);
|
|
269
|
+
tabInfo.group.tabs.splice(args.toIndex, 0, tabDto);
|
|
270
|
+
this.proxy.$acceptTabOperation({
|
|
271
|
+
kind: TabModelOperationKind.TAB_MOVE,
|
|
272
|
+
index: args.toIndex,
|
|
273
|
+
tabDto,
|
|
274
|
+
groupId: tabInfo.group.groupId,
|
|
275
|
+
oldIndex: args.fromIndex
|
|
276
|
+
});
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
private onTabGroupCreated(tabBar: TabBar<Widget>): void {
|
|
280
|
+
this.tabGroupChanged = true;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private onTabGroupClosed(tabBar: TabBar<Widget>): void {
|
|
284
|
+
this.tabGroupChanged = true;
|
|
285
|
+
}
|
|
286
|
+
// #endregion
|
|
28
287
|
|
|
29
288
|
// #region Messages received from Ext Host
|
|
30
289
|
$moveTab(tabId: string, index: number, viewColumn: number, preserveFocus?: boolean): void {
|
|
@@ -32,11 +291,33 @@ export class TabsMainImp implements TabsMain {
|
|
|
32
291
|
}
|
|
33
292
|
|
|
34
293
|
async $closeTab(tabIds: string[], preserveFocus?: boolean): Promise<boolean> {
|
|
35
|
-
|
|
294
|
+
const widgets: Widget[] = [];
|
|
295
|
+
for (const tabId of tabIds) {
|
|
296
|
+
const cleanedId = tabId.substring(tabId.indexOf('~') + 1);
|
|
297
|
+
const widget = this.applicationShell.getWidgetById(cleanedId);
|
|
298
|
+
if (widget) {
|
|
299
|
+
widgets.push(widget);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
await this.applicationShell.closeMany(widgets);
|
|
303
|
+
return true;
|
|
36
304
|
}
|
|
37
305
|
|
|
38
306
|
async $closeGroup(groupIds: number[], preserveFocus?: boolean): Promise<boolean> {
|
|
39
|
-
|
|
307
|
+
for (const groupId of groupIds) {
|
|
308
|
+
tabGroupModel: for (const [bar, groupDto] of this.tabGroupModel) {
|
|
309
|
+
if (groupDto.groupId === groupId) {
|
|
310
|
+
this.applicationShell.closeTabs(bar);
|
|
311
|
+
break tabGroupModel;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
return true;
|
|
40
316
|
}
|
|
41
317
|
// #endregion
|
|
318
|
+
|
|
319
|
+
dispose(): void {
|
|
320
|
+
this.toDisposeOnDestroy.dispose();
|
|
321
|
+
this.disposableTabBarListeners.dispose();
|
|
322
|
+
}
|
|
42
323
|
}
|
package/src/plugin/tabs.ts
CHANGED
|
@@ -291,14 +291,6 @@ export class TabsExtImpl implements TabsExt {
|
|
|
291
291
|
return this._closeTabs(tabsOrTabGroups as theia.Tab[], preserveFocus);
|
|
292
292
|
}
|
|
293
293
|
},
|
|
294
|
-
// move: async (tab: theia.Tab, viewColumn: ViewColumn, index: number, preserveFocus?: boolean) => {
|
|
295
|
-
// const extHostTab = this._findExtHostTabFromApi(tab);
|
|
296
|
-
// if (!extHostTab) {
|
|
297
|
-
// throw new Error('Invalid tab');
|
|
298
|
-
// }
|
|
299
|
-
// this._proxy.$moveTab(extHostTab.tabId, index, typeConverters.ViewColumn.from(viewColumn), preserveFocus);
|
|
300
|
-
// return;
|
|
301
|
-
// }
|
|
302
294
|
};
|
|
303
295
|
this.apiObject = Object.freeze(obj);
|
|
304
296
|
}
|
|
@@ -306,7 +298,6 @@ export class TabsExtImpl implements TabsExt {
|
|
|
306
298
|
}
|
|
307
299
|
|
|
308
300
|
$acceptEditorTabModel(tabGroups: TabGroupDto[]): void {
|
|
309
|
-
|
|
310
301
|
const groupIdsBefore = new Set(this.tabGroupArr.map(group => group.groupId));
|
|
311
302
|
const groupIdsAfter = new Set(tabGroups.map(dto => dto.groupId));
|
|
312
303
|
const diff = diffSets(groupIdsBefore, groupIdsAfter);
|
|
@@ -314,23 +305,28 @@ export class TabsExtImpl implements TabsExt {
|
|
|
314
305
|
const closed: theia.TabGroup[] = this.tabGroupArr.filter(group => diff.removed.includes(group.groupId)).map(group => group.apiObject);
|
|
315
306
|
const opened: theia.TabGroup[] = [];
|
|
316
307
|
const changed: theia.TabGroup[] = [];
|
|
308
|
+
const tabsOpened: theia.Tab[] = [];
|
|
317
309
|
|
|
318
310
|
this.tabGroupArr = tabGroups.map(tabGroup => {
|
|
319
311
|
const group = new TabGroupExt(tabGroup, () => this.activeGroupId);
|
|
320
312
|
if (diff.added.includes(group.groupId)) {
|
|
321
|
-
opened.push(group.apiObject);
|
|
313
|
+
opened.push({ activeTab: undefined, isActive: group.apiObject.isActive, tabs: [], viewColumn: group.apiObject.viewColumn });
|
|
314
|
+
tabsOpened.push(...group.apiObject.tabs);
|
|
322
315
|
} else {
|
|
323
316
|
changed.push(group.apiObject);
|
|
324
317
|
}
|
|
325
318
|
return group;
|
|
326
319
|
});
|
|
327
320
|
|
|
328
|
-
// Set the active tab group id
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
this.activeGroupId
|
|
321
|
+
// Set the active tab group id. skip if no tabgroups are open
|
|
322
|
+
if (tabGroups.length > 0) {
|
|
323
|
+
const activeTabGroupId = assertIsDefined(tabGroups.find(group => group.isActive === true)?.groupId);
|
|
324
|
+
if (this.activeGroupId !== activeTabGroupId) {
|
|
325
|
+
this.activeGroupId = activeTabGroupId;
|
|
326
|
+
}
|
|
332
327
|
}
|
|
333
328
|
this.onDidChangeTabGroups.fire(Object.freeze({ opened, closed, changed }));
|
|
329
|
+
this.onDidChangeTabs.fire({ opened: tabsOpened, changed: [], closed: [] });
|
|
334
330
|
}
|
|
335
331
|
|
|
336
332
|
$acceptTabGroupUpdate(groupDto: TabGroupDto): void {
|