Skip to content

Commit 690007d

Browse files
Eric Amodioeamodio
authored andcommitted
Closes microsoft#92135 - Adds view-level progress indicator
1 parent 89fe6d2 commit 690007d

12 files changed

Lines changed: 309 additions & 2 deletions

File tree

src/vs/workbench/browser/parts/compositePart.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,10 @@ export abstract class CompositePart<T extends Composite> extends Part {
158158
return composite;
159159
}
160160

161+
protected getInstantiatedComposite(id: string) {
162+
return this.instantiatedCompositeItems.get(id)?.composite;
163+
}
164+
161165
protected createComposite(id: string, isActive?: boolean): Composite {
162166

163167
// Check if composite is already created

src/vs/workbench/browser/parts/panel/panelPart.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -434,6 +434,10 @@ export class PanelPart extends CompositePart<Panel> implements IPanelService {
434434
return this.getActiveComposite();
435435
}
436436

437+
getInstantiatedPanel(id: string): IPanel | undefined {
438+
return this.getInstantiatedComposite(id) as IPanel | undefined;
439+
}
440+
437441
getLastActivePanelId(): string {
438442
return this.getLastActiveCompositetId();
439443
}

src/vs/workbench/browser/parts/sidebar/sidebarPart.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ export class SidebarPart extends CompositePart<Viewlet> implements IViewletServi
225225
return <IViewlet>this.getActiveComposite();
226226
}
227227

228+
getInstantiatedViewlet(id: string): IViewlet | undefined {
229+
return this.getInstantiatedComposite(id) as IViewlet | undefined;
230+
}
231+
228232
getLastActiveViewletId(): string {
229233
return this.getLastActiveCompositetId();
230234
}

src/vs/workbench/browser/parts/views/media/paneviewlet.css

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@
77
border-top: none !important; /* less clutter: do not show any border for first views in a pane */
88
}
99

10+
.monaco-pane-view .pane > .pane-header {
11+
position: relative;
12+
}
13+
1014
.monaco-pane-view .pane > .pane-header > .actions.show {
1115
display: initial;
1216
}
@@ -23,3 +27,11 @@
2327
.monaco-pane-view .pane > .pane-header h3.title:first-child {
2428
margin-left: 7px;
2529
}
30+
31+
.monaco-pane-view .pane > .pane-header .monaco-progress-container {
32+
position: absolute;
33+
left: 0;
34+
bottom: 0;
35+
z-index: 5;
36+
height: 2px;
37+
}

src/vs/workbench/browser/parts/views/viewPaneContainer.ts

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import 'vs/css!./media/paneviewlet';
77
import * as nls from 'vs/nls';
88
import { Event, Emitter } from 'vs/base/common/event';
99
import { ColorIdentifier } from 'vs/platform/theme/common/colorRegistry';
10-
import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler } from 'vs/platform/theme/common/styler';
10+
import { attachStyler, IColorMapping, attachButtonStyler, attachLinkStyler, attachProgressBarStyler } from 'vs/platform/theme/common/styler';
1111
import { SIDE_BAR_DRAG_AND_DROP_BACKGROUND, SIDE_BAR_SECTION_HEADER_FOREGROUND, SIDE_BAR_SECTION_HEADER_BACKGROUND, SIDE_BAR_SECTION_HEADER_BORDER, PANEL_BACKGROUND, SIDE_BAR_BACKGROUND } from 'vs/workbench/common/theme';
1212
import { append, $, trackFocus, toggleClass, EventType, isAncestor, Dimension, addDisposableListener, removeClass, addClass } from 'vs/base/browser/dom';
1313
import { IDisposable, combinedDisposable, dispose, toDisposable, Disposable, DisposableStore } from 'vs/base/common/lifecycle';
@@ -44,6 +44,9 @@ import { Button } from 'vs/base/browser/ui/button/button';
4444
import { Link } from 'vs/platform/opener/browser/link';
4545
import { LocalSelectionTransfer } from 'vs/workbench/browser/dnd';
4646
import { Orientation } from 'vs/base/browser/ui/sash/sash';
47+
import { ProgressBar } from 'vs/base/browser/ui/progressbar/progressbar';
48+
import { ViewProgressIndicator } from 'vs/workbench/services/progress/browser/progressIndicator';
49+
import { IProgressIndicator } from 'vs/platform/progress/common/progress';
4750

4851
export interface IPaneColors extends IColorMapping {
4952
dropBackground?: ColorIdentifier;
@@ -181,6 +184,8 @@ export abstract class ViewPane extends Pane implements IView {
181184
title: string;
182185

183186
private readonly menuActions: ViewMenuActions;
187+
private progressBar!: ProgressBar;
188+
private progressIndicator!: IProgressIndicator;
184189

185190
private toolbar?: ToolBar;
186191
private readonly showActionsAlways: boolean = false;
@@ -289,6 +294,13 @@ export abstract class ViewPane extends Pane implements IView {
289294
const onDidRelevantConfigurationChange = Event.filter(this.configurationService.onDidChangeConfiguration, e => e.affectsConfiguration(ViewPane.AlwaysShowActionsConfig));
290295
this._register(onDidRelevantConfigurationChange(this.updateActionsVisibility, this));
291296
this.updateActionsVisibility();
297+
298+
if (this.progressBar !== undefined) {
299+
// Progress bar
300+
this.progressBar = this._register(new ProgressBar(this.headerContainer));
301+
this._register(attachProgressBarStyler(this.progressBar, this.themeService));
302+
this.progressBar.hide();
303+
}
292304
}
293305

294306
protected renderTwisties(container: HTMLElement): void {
@@ -320,6 +332,24 @@ export abstract class ViewPane extends Pane implements IView {
320332
// noop
321333
}
322334

335+
getProgressIndicator() {
336+
if (!this.headerContainer) {
337+
return undefined;
338+
}
339+
340+
if (this.progressBar === undefined) {
341+
// Progress bar
342+
this.progressBar = this._register(new ProgressBar(this.headerContainer));
343+
this._register(attachProgressBarStyler(this.progressBar, this.themeService));
344+
this.progressBar.hide();
345+
}
346+
347+
if (this.progressIndicator === undefined) {
348+
this.progressIndicator = this.instantiationService.createInstance(ViewProgressIndicator, this, assertIsDefined(this.progressBar));
349+
}
350+
return this.progressIndicator;
351+
}
352+
323353
protected getProgressLocation(): string {
324354
return this.viewDescriptorService.getViewContainer(this.id)!.id;
325355
}

src/vs/workbench/browser/parts/views/views.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ import { Viewlet, ViewletDescriptor, ViewletRegistry, Extensions as ViewletExten
3434
import { IConfigurationService } from 'vs/platform/configuration/common/configuration';
3535
import { IWorkbenchLayoutService } from 'vs/workbench/services/layout/browser/layoutService';
3636
import { URI } from 'vs/base/common/uri';
37+
import { IProgressIndicator } from 'vs/platform/progress/common/progress';
3738

3839
export interface IViewState {
3940
visibleGlobal: boolean | undefined;
@@ -699,6 +700,27 @@ export class ViewsService extends Disposable implements IViewsService {
699700
return null;
700701
}
701702

703+
getProgressIndicator(id: string): IProgressIndicator | undefined {
704+
const viewContainer = this.viewDescriptorService.getViewContainer(id);
705+
if (viewContainer === null) {
706+
return undefined;
707+
}
708+
709+
const location = this.viewContainersRegistry.getViewContainerLocation(viewContainer);
710+
711+
let viewPaneContainer;
712+
if (location === ViewContainerLocation.Sidebar) {
713+
const viewlet = this.viewletService.getInstantiatedViewlet(viewContainer.id);
714+
viewPaneContainer = viewlet?.getViewPaneContainer();
715+
} else if (location === ViewContainerLocation.Panel) {
716+
const panel = this.panelService.getInstantiatedPanel(viewContainer.id);
717+
viewPaneContainer = (panel as IPaneComposite)?.getViewPaneContainer();
718+
}
719+
720+
const view = viewPaneContainer?.getView(id);
721+
return view?.getProgressIndicator();
722+
}
723+
702724
private registerViewletOrPanel(viewContainer: ViewContainer, viewContainerLocation: ViewContainerLocation): void {
703725
switch (viewContainerLocation) {
704726
case ViewContainerLocation.Panel:

src/vs/workbench/common/views.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import { ExtensionIdentifier } from 'vs/platform/extensions/common/extensions';
2020
import { flatten, mergeSort } from 'vs/base/common/arrays';
2121
import { SyncDescriptor } from 'vs/platform/instantiation/common/descriptors';
2222
import { SetMap } from 'vs/base/common/collections';
23+
import { IProgressIndicator } from 'vs/platform/progress/common/progress';
2324

2425
export const TEST_VIEW_CONTAINER_ID = 'workbench.view.extension.test';
2526

@@ -402,6 +403,7 @@ export interface IView {
402403

403404
setExpanded(expanded: boolean): boolean;
404405

406+
getProgressIndicator(): IProgressIndicator | undefined;
405407
}
406408

407409
export interface IViewsViewlet extends IViewlet {
@@ -426,6 +428,7 @@ export interface IViewsService {
426428

427429
closeView(id: string): void;
428430

431+
getProgressIndicator(id: string): IProgressIndicator | undefined;
429432
}
430433

431434
/**

src/vs/workbench/services/panel/common/panelService.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,11 @@ export interface IPanelService {
3535
*/
3636
getActivePanel(): IPanel | undefined;
3737

38+
/**
39+
* Returns an instantiated panel by id, if any.
40+
*/
41+
getInstantiatedPanel(id: string): IPanel | undefined;
42+
3843
/**
3944
* Returns the panel by id.
4045
*/

src/vs/workbench/services/progress/browser/progressIndicator.ts

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import { IViewletService } from 'vs/workbench/services/viewlet/browser/viewlet';
1010
import { IPanelService } from 'vs/workbench/services/panel/common/panelService';
1111
import { IProgressRunner, IProgressIndicator, emptyProgressRunner } from 'vs/platform/progress/common/progress';
1212
import { IEditorGroupView } from 'vs/workbench/browser/parts/editor/editor';
13+
import { ViewPane } from 'vs/workbench/browser/parts/views/viewPaneContainer';
1314

1415
export class ProgressBarIndicator extends Disposable implements IProgressIndicator {
1516

@@ -350,3 +351,160 @@ export class CompositeProgressIndicator extends CompositeScope implements IProgr
350351
}
351352
}
352353
}
354+
355+
export class ViewProgressIndicator extends Disposable implements IProgressIndicator {
356+
private progressState: ProgressIndicatorState.State = ProgressIndicatorState.None;
357+
358+
private isVisible: boolean;
359+
360+
constructor(
361+
view: ViewPane,
362+
private progressbar: ProgressBar,
363+
) {
364+
super();
365+
366+
this.progressbar = progressbar;
367+
this._register(view.onDidChangeBodyVisibility(this.onVisible, this));
368+
this.isVisible = view.isVisible();
369+
}
370+
371+
onVisible(visible: boolean) {
372+
this.isVisible = visible;
373+
374+
// Return early if progress state indicates that progress is done
375+
if (!visible || this.progressState.type === ProgressIndicatorState.Done.type) {
376+
this.progressbar.stop().hide();
377+
return;
378+
}
379+
380+
// Replay Infinite Progress from Promise
381+
if (this.progressState.type === ProgressIndicatorState.Type.While) {
382+
let delay: number | undefined;
383+
if (this.progressState.whileDelay > 0) {
384+
const remainingDelay = this.progressState.whileDelay - (Date.now() - this.progressState.whileStart);
385+
if (remainingDelay > 0) {
386+
delay = remainingDelay;
387+
}
388+
}
389+
390+
this.doShowWhile(delay);
391+
}
392+
393+
// Replay Infinite Progress
394+
else if (this.progressState.type === ProgressIndicatorState.Type.Infinite) {
395+
this.progressbar.infinite().show();
396+
}
397+
398+
// Replay Finite Progress (Total & Worked)
399+
else if (this.progressState.type === ProgressIndicatorState.Type.Work) {
400+
if (this.progressState.total) {
401+
this.progressbar.total(this.progressState.total).show();
402+
}
403+
404+
if (this.progressState.worked) {
405+
this.progressbar.worked(this.progressState.worked).show();
406+
}
407+
}
408+
}
409+
410+
show(infinite: true, delay?: number): IProgressRunner;
411+
show(total: number, delay?: number): IProgressRunner;
412+
show(infiniteOrTotal: true | number, delay?: number): IProgressRunner {
413+
414+
// Sort out Arguments
415+
if (typeof infiniteOrTotal === 'boolean') {
416+
this.progressState = ProgressIndicatorState.Infinite;
417+
} else {
418+
this.progressState = new ProgressIndicatorState.Work(infiniteOrTotal, undefined);
419+
}
420+
421+
// Active: Show Progress
422+
if (this.isVisible) {
423+
424+
// Infinite: Start Progressbar and Show after Delay
425+
if (this.progressState.type === ProgressIndicatorState.Type.Infinite) {
426+
this.progressbar.infinite().show(delay);
427+
}
428+
429+
// Finite: Start Progressbar and Show after Delay
430+
else if (this.progressState.type === ProgressIndicatorState.Type.Work && typeof this.progressState.total === 'number') {
431+
this.progressbar.total(this.progressState.total).show(delay);
432+
}
433+
}
434+
435+
return {
436+
total: (total: number) => {
437+
this.progressState = new ProgressIndicatorState.Work(
438+
total,
439+
this.progressState.type === ProgressIndicatorState.Type.Work ? this.progressState.worked : undefined);
440+
441+
if (this.isVisible) {
442+
this.progressbar.total(total);
443+
}
444+
},
445+
446+
worked: (worked: number) => {
447+
448+
// Verify first that we are either not active or the progressbar has a total set
449+
if (!this.isVisible || this.progressbar.hasTotal()) {
450+
this.progressState = new ProgressIndicatorState.Work(
451+
this.progressState.type === ProgressIndicatorState.Type.Work ? this.progressState.total : undefined,
452+
this.progressState.type === ProgressIndicatorState.Type.Work && typeof this.progressState.worked === 'number' ? this.progressState.worked + worked : worked);
453+
454+
if (this.isVisible) {
455+
this.progressbar.worked(worked);
456+
}
457+
}
458+
459+
// Otherwise the progress bar does not support worked(), we fallback to infinite() progress
460+
else {
461+
this.progressState = ProgressIndicatorState.Infinite;
462+
this.progressbar.infinite().show();
463+
}
464+
},
465+
466+
done: () => {
467+
this.progressState = ProgressIndicatorState.Done;
468+
469+
this.progressbar.stop().hide();
470+
}
471+
};
472+
}
473+
474+
async showWhile(promise: Promise<unknown>, delay?: number): Promise<void> {
475+
476+
// Join with existing running promise to ensure progress is accurate
477+
if (this.progressState.type === ProgressIndicatorState.Type.While) {
478+
promise = Promise.all([promise, this.progressState.whilePromise]);
479+
}
480+
481+
// Keep Promise in State
482+
this.progressState = new ProgressIndicatorState.While(promise, delay || 0, Date.now());
483+
484+
try {
485+
this.doShowWhile(delay);
486+
487+
await promise;
488+
} catch (error) {
489+
// ignore
490+
} finally {
491+
492+
// If this is not the last promise in the list of joined promises, skip this
493+
if (this.progressState.type !== ProgressIndicatorState.Type.While || this.progressState.whilePromise === promise) {
494+
495+
// The while promise is either null or equal the promise we last hooked on
496+
this.progressState = ProgressIndicatorState.None;
497+
498+
this.progressbar.stop().hide();
499+
}
500+
}
501+
}
502+
503+
private doShowWhile(delay?: number): void {
504+
505+
// Show Progress when active
506+
if (this.isVisible) {
507+
this.progressbar.infinite().show(delay);
508+
}
509+
}
510+
}

0 commit comments

Comments
 (0)