@theia/plugin-ext 1.77.0-next.4 → 1.77.0-next.47

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.
@@ -23,7 +23,7 @@ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/front
23
23
  FrontendApplicationConfigProvider.set({});
24
24
 
25
25
  import { expect } from 'chai';
26
- import { Disposable } from '@theia/core/lib/common';
26
+ import { Command, Disposable } from '@theia/core/lib/common';
27
27
  import { PluginViewRegistry, ViewContainerInfo, PLUGIN_VIEW_DATA_FACTORY_ID } from './plugin-view-registry';
28
28
  import type { ViewWelcome } from '../../../common';
29
29
 
@@ -54,7 +54,6 @@ describe('PluginViewRegistry - view menu labels', () => {
54
54
  let registry: PluginViewRegistry;
55
55
  let menus: RecordingMenuModelRegistry;
56
56
 
57
- const toggleCommandId = (id: string): string => `plugin.view-container.${id}.toggle`;
58
57
  const internals = (): {
59
58
  viewContainers: Map<string, ViewContainerInfo>;
60
59
  registerViewMenuAction(containerId: string, label: string): Disposable;
@@ -76,21 +75,21 @@ describe('PluginViewRegistry - view menu labels', () => {
76
75
 
77
76
  it('uses the plain label for a single container', () => {
78
77
  registerContainer('a', 'Claude Code', 'left');
79
- expect(menus.actions.get(toggleCommandId('a'))?.label).to.equal('Claude Code');
78
+ expect(menus.actions.get('a')?.label).to.equal('Claude Code');
80
79
  });
81
80
 
82
81
  it('leaves distinct labels unsuffixed', () => {
83
82
  registerContainer('a', 'Explorer', 'left');
84
83
  registerContainer('b', 'Claude Code', 'right');
85
- expect(menus.actions.get(toggleCommandId('a'))?.label).to.equal('Explorer');
86
- expect(menus.actions.get(toggleCommandId('b'))?.label).to.equal('Claude Code');
84
+ expect(menus.actions.get('a')?.label).to.equal('Explorer');
85
+ expect(menus.actions.get('b')?.label).to.equal('Claude Code');
87
86
  });
88
87
 
89
88
  it('suffixes the location when two containers share a label', () => {
90
89
  registerContainer('a', 'Claude Code', 'left');
91
90
  registerContainer('b', 'Claude Code', 'right');
92
- expect(menus.actions.get(toggleCommandId('a'))?.label).to.equal('Claude Code (Side Bar)');
93
- expect(menus.actions.get(toggleCommandId('b'))?.label).to.equal('Claude Code (Secondary Side Bar)');
91
+ expect(menus.actions.get('a')?.label).to.equal('Claude Code (Side Bar)');
92
+ expect(menus.actions.get('b')?.label).to.equal('Claude Code (Secondary Side Bar)');
94
93
  });
95
94
 
96
95
  it('drops the suffix from the remaining container once the duplicate is removed', () => {
@@ -99,8 +98,8 @@ describe('PluginViewRegistry - view menu labels', () => {
99
98
 
100
99
  disposeB.dispose();
101
100
 
102
- expect(menus.actions.get(toggleCommandId('a'))?.label).to.equal('Claude Code');
103
- expect(menus.actions.has(toggleCommandId('b'))).to.equal(false);
101
+ expect(menus.actions.get('a')?.label).to.equal('Claude Code');
102
+ expect(menus.actions.has('b')).to.equal(false);
104
103
  });
105
104
 
106
105
  });
@@ -171,3 +170,46 @@ describe('PluginViewRegistry - welcome-only views', () => {
171
170
  });
172
171
 
173
172
  });
173
+
174
+ describe('PluginViewRegistry - VS Code view commands', () => {
175
+
176
+ let registry: PluginViewRegistry;
177
+ let commands: Map<string, Command>;
178
+
179
+ const internals = (): {
180
+ doRegisterViewContainer(id: string, location: string, options: { label: string }): Disposable;
181
+ } => registry as unknown as {
182
+ doRegisterViewContainer(id: string, location: string, options: { label: string }): Disposable;
183
+ };
184
+
185
+ beforeEach(() => {
186
+ registry = new PluginViewRegistry();
187
+ commands = new Map();
188
+ (registry as unknown as { commands: unknown }).commands = {
189
+ registerCommand: (command: Command) => {
190
+ commands.set(command.id, command);
191
+ return Disposable.create(() => commands.delete(command.id));
192
+ }
193
+ };
194
+ (registry as unknown as { menus: unknown }).menus = new RecordingMenuModelRegistry();
195
+ (registry as unknown as { quickView: unknown }).quickView = { registerItem: () => Disposable.NULL };
196
+ });
197
+
198
+ it('registers the container id and the view focus and open commands once a view is added', () => {
199
+ const containerId = 'workbench.view.extension.sample';
200
+ internals().doRegisterViewContainer(containerId, 'left', { label: 'Sample' });
201
+ expect(commands.has(containerId)).to.equal(false);
202
+
203
+ const disposeView = registry.registerView('sample', { id: 'sample.view', name: 'Sample View' });
204
+
205
+ expect(commands.get(containerId)).to.include({ label: 'Toggle Sample', category: 'View' });
206
+ expect(commands.get('plugin.view-container.workbench.view.extension.sample.toggle')?.label).to.equal(undefined);
207
+ expect(commands.get('sample.view.focus')).to.include({ label: 'Focus on Sample View View', category: 'Sample' });
208
+ expect(commands.has('sample.view.open')).to.equal(true);
209
+
210
+ disposeView.dispose();
211
+ expect(commands.has('sample.view.focus')).to.equal(false);
212
+ expect(commands.has('sample.view.open')).to.equal(false);
213
+ });
214
+
215
+ });
@@ -347,15 +347,30 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
347
347
  return toDispose;
348
348
  }
349
349
 
350
+ /**
351
+ * Toggles the view container the same way as {@link AbstractViewContribution.toggleView}:
352
+ * collapses its panel if it is the visible tab, otherwise opens and activates it.
353
+ */
350
354
  protected async toggleViewContainer(id: string): Promise<void> {
351
- let widget = await this.getPluginViewContainer(id);
352
- if (widget && widget.isAttached) {
353
- widget.dispose();
354
- } else {
355
- widget = await this.openViewContainer(id);
356
- if (widget) {
357
- this.shell.activateWidget(widget.id);
355
+ const existing = await this.getPluginViewContainer(id);
356
+ const tabBar = existing && this.shell.getTabBarFor(existing);
357
+ const area = existing && this.shell.getAreaFor(existing);
358
+ if (existing && tabBar && area && this.shell.isExpanded(area) && tabBar.currentTitle === existing.title) {
359
+ if (area === 'left' || area === 'right') {
360
+ await this.shell.collapsePanel(area);
361
+ } else if (area === 'bottom') {
362
+ // Don't collapse the bottom panel if it's currently split
363
+ if (this.shell.bottomAreaTabBars.length === 1) {
364
+ await this.shell.collapsePanel('bottom');
365
+ }
366
+ } else {
367
+ existing.dispose();
358
368
  }
369
+ return;
370
+ }
371
+ const widget = await this.openViewContainer(id);
372
+ if (widget) {
373
+ await this.shell.activateWidget(widget.id);
359
374
  }
360
375
  }
361
376
 
@@ -369,18 +384,22 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
369
384
  toDispose.push(Disposable.create(() => this.viewContainerClauseContexts.delete(id)));
370
385
  }
371
386
  }
372
- const toggleCommandId = `plugin.view-container.${id}.toggle`;
373
387
  // Some plugins may register empty view containers.
374
388
  // We should not register commands for them immediately, as that leads to bad UX.
375
389
  // Instead, we register commands the first time we add a view to them.
376
390
  let activate = () => {
391
+ // Like VS Code, use the container's id, e.g. `workbench.view.extension.${id}`, as the command id
377
392
  toDispose.push(this.commands.registerCommand({
378
- id: toggleCommandId,
393
+ id,
379
394
  category: nls.localizeByDefault('View'),
380
395
  label: nls.localizeByDefault('Toggle {0}', options.label)
381
396
  }, {
382
397
  execute: () => this.toggleViewContainer(id)
383
398
  }));
399
+ // Kept for backward compatibility with keybindings that use the former command id
400
+ toDispose.push(this.commands.registerCommand({ id: `plugin.view-container.${id}.toggle` }, {
401
+ execute: () => this.toggleViewContainer(id)
402
+ }));
384
403
  toDispose.push(this.registerViewMenuAction(id, options.label));
385
404
  toDispose.push(this.quickView?.registerItem({
386
405
  label: options.label,
@@ -486,7 +505,7 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
486
505
  }
487
506
  }
488
507
  const disposable = this.menus.registerMenuAction(CommonMenus.VIEW_VIEWS, {
489
- commandId: `plugin.view-container.${id}.toggle`,
508
+ commandId: id,
490
509
  label: menuLabel,
491
510
  when: containerInfo.when
492
511
  });
@@ -561,15 +580,24 @@ export class PluginViewRegistry implements FrontendApplicationContribution {
561
580
  when: view.when,
562
581
  open: () => this.openView(view.id, { activate: true })
563
582
  }));
564
- toDispose.push(this.commands.registerCommand({ id: `${view.id}.focus` }, {
565
- execute: async () => { await this.openView(view.id, { activate: true }); }
583
+ toDispose.push(this.commands.registerCommand({
584
+ id: `${view.id}.focus`,
585
+ category: this.viewContainers.get(viewContainerId)?.options.label,
586
+ label: nls.localizeByDefault('Focus on {0} View', view.name)
587
+ }, {
588
+ execute: async (options?: { preserveFocus?: boolean }) => { await this.openView(view.id, this.toOpenViewOptions(options)); },
589
+ isVisible: () => this.isViewVisible(view.id)
566
590
  }));
567
591
  toDispose.push(this.commands.registerCommand({ id: `${view.id}.open` }, {
568
- execute: async () => { await this.openView(view.id, { activate: true }); }
592
+ execute: async (options?: { preserveFocus?: boolean }) => { await this.openView(view.id, this.toOpenViewOptions(options)); }
569
593
  }));
570
594
  return toDispose;
571
595
  }
572
596
 
597
+ protected toOpenViewOptions(options?: { preserveFocus?: boolean }): { activate?: boolean, reveal?: boolean } {
598
+ return options?.preserveFocus ? { reveal: true } : { activate: true };
599
+ }
600
+
573
601
  async resolveWebviewView(viewId: string, webview: WebviewView, cancellation: CancellationToken): Promise<void> {
574
602
  const resolver = this.webviewViewResolvers.get(viewId);
575
603
  if (resolver) {
@@ -504,7 +504,7 @@ export function createAPIFactory(
504
504
  preserveFocus?: boolean
505
505
  ): Promise<theia.TextEditor> {
506
506
  let documentOptions: theia.TextDocumentShowOptions | undefined;
507
- const uri: URI = documentArg instanceof URI ? documentArg : documentArg.uri;
507
+ const uri: URI = URI.isUri(documentArg) ? URI.revive(documentArg) : documentArg.uri;
508
508
  if (typeof columnOrOptions === 'number') {
509
509
  documentOptions = {
510
510
  viewColumn: columnOrOptions
@@ -65,6 +65,31 @@ describe('API Type Implementations:', () => {
65
65
  types.URI.joinPath(types.URI.from({ scheme: 'myScheme', authority: 'authority', path: '/path', query: 'query', fragment: 'fragment' }), '/file.js').toString(),
66
66
  'myScheme://authority/path/file.js?query#fragment');
67
67
  });
68
+
69
+ // Issue: #14976
70
+ it('should return an instance of the API URI class from every factory', () => {
71
+ const components = { scheme: 'file', authority: '', path: '/foo/bar.js', query: '', fragment: '' };
72
+ assert.ok(types.URI.parse('file:///foo/bar.js') instanceof types.URI, 'parse');
73
+ assert.ok(types.URI.file('/foo/bar.js') instanceof types.URI, 'file');
74
+ assert.ok(types.URI.revive(components) instanceof types.URI, 'revive');
75
+ assert.ok(types.URI.from(components) instanceof types.URI, 'from');
76
+ assert.ok(types.URI.joinPath(types.URI.file('/foo'), 'bar.js') instanceof types.URI, 'joinPath');
77
+ assert.ok(types.URI.file('/foo/bar.js').with({ fragment: 'baz' }) instanceof types.URI, 'with');
78
+ });
79
+
80
+ it('should build an equivalent URI with from()', () => {
81
+ const uriString = 'scheme://authority.com/foo/bar/zoz?query#fragment';
82
+ const uri = types.URI.parse(uriString);
83
+ const from = types.URI.from({
84
+ scheme: uri.scheme,
85
+ authority: uri.authority,
86
+ path: uri.path,
87
+ query: uri.query,
88
+ fragment: uri.fragment
89
+ });
90
+
91
+ assert.strictEqual(from.toString(), uriString);
92
+ });
68
93
  });
69
94
 
70
95
  describe('RelativePattern:', () => {
@@ -95,6 +95,13 @@ export class URI extends CodeURI implements theia.Uri {
95
95
  return new URI(CodeURI.file(path));
96
96
  }
97
97
 
98
+ /**
99
+ * Override to create the correct class.
100
+ */
101
+ static override from(components: { scheme: string; authority?: string; path?: string; query?: string; fragment?: string }): URI {
102
+ return new URI(CodeURI.from(components));
103
+ }
104
+
98
105
  /**
99
106
  * There is quite some magic in to vscode URI class related to
100
107
  * transferring via JSON.stringify(). Making the CodeURI instance