@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.
@@ -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
- export class TabsMainImp implements TabsMain {
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
- return false;
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
- return false;
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
  }
@@ -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
- const activeTabGroupId = assertIsDefined(tabGroups.find(group => group.isActive === true)?.groupId);
330
- if (activeTabGroupId !== undefined && this.activeGroupId !== activeTabGroupId) {
331
- this.activeGroupId = activeTabGroupId;
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 {