Skip to content

Commit 00f85cf

Browse files
ErwanDLErwan de Lépinau
andauthored
Replace spinner with custom icon when downloading MPLS/Insiders build (#10687)
* replace default Progress with custom icon when downloading * extract formatProgressMessageWithState method for readability * add news entry for #10495 * move icon display logic to `downloadFileWithStatusBarProgress` * move RequestProgressState to fileDownloader and move formatProgressMessageWithState at module level * fix try block to also catch download initialization errors * add withProgressCustomIcon method * use withProgressCustomIcon in fileDownloader.ts * fix withProgressCustomIcon * adapt fileDownloader unit tests * add doc for icon param * drop param `options` in withProgressCustomIcon * update test_plan Co-authored-by: Erwan de Lépinau <[email protected]>
1 parent 2e521ab commit 00f85cf

File tree

7 files changed

+86
-18
lines changed

7 files changed

+86
-18
lines changed

.github/test_plan.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ SPAM='hello ${WHO}'
135135
#### Language server
136136

137137
- [ ] LS is downloaded using HTTP (no SSL) when the "http.proxyStrictSSL" setting is false
138+
- [ ] An item with a cloud icon appears in the status bar indicating progress while downloading the language server
138139
- [ ] Installing [`requests`](https://pypi.org/project/requests/) in virtual environment is detected
139140
- [ ] Import of `requests` without package installed is flagged as unresolved
140141
- [ ] Create a virtual environment

news/1 Enhancements/10495.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Use specific icons when downloading MPLS and Insiders instead of the spinner.

src/client/common/application/applicationShell.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import { injectable } from 'inversify';
88
import {
99
CancellationToken,
10+
CancellationTokenSource,
1011
Disposable,
1112
env,
1213
Event,
@@ -121,6 +122,23 @@ export class ApplicationShell implements IApplicationShell {
121122
): Thenable<R> {
122123
return window.withProgress<R>(options, task);
123124
}
125+
public withProgressCustomIcon<R>(
126+
icon: string,
127+
task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable<R>
128+
): Thenable<R> {
129+
const token = new CancellationTokenSource().token;
130+
const statusBarProgress = this.createStatusBarItem(StatusBarAlignment.Left);
131+
const progress = {
132+
report: (value: { message?: string; increment?: number }) => {
133+
statusBarProgress.text = `${icon} ${value.message}`;
134+
}
135+
};
136+
statusBarProgress.show();
137+
return task(progress, token).then((result) => {
138+
statusBarProgress.dispose();
139+
return result;
140+
});
141+
}
124142
public createQuickPick<T extends QuickPickItem>(): QuickPick<T> {
125143
return window.createQuickPick<T>();
126144
}

src/client/common/application/types.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -374,6 +374,33 @@ export interface IApplicationShell {
374374
task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable<R>
375375
): Thenable<R>;
376376

377+
/**
378+
* Show progress in the status bar with a custom icon instead of the default spinner.
379+
* Progress is shown while running the given callback and while the promise it returned isn't resolved nor rejected.
380+
* At the moment, progress can only be displayed in the status bar when using this method. If you want to
381+
* display it elsewhere, use `withProgress`.
382+
*
383+
* @param icon A valid Octicon.
384+
*
385+
* @param task A callback returning a promise. Progress state can be reported with
386+
* the provided [progress](#Progress)-object.
387+
*
388+
* To report discrete progress, use `increment` to indicate how much work has been completed. Each call with
389+
* a `increment` value will be summed up and reflected as overall progress until 100% is reached (a value of
390+
* e.g. `10` accounts for `10%` of work done).
391+
* Note that currently only `ProgressLocation.Notification` is capable of showing discrete progress.
392+
*
393+
* To monitor if the operation has been cancelled by the user, use the provided [`CancellationToken`](#CancellationToken).
394+
* Note that currently only `ProgressLocation.Notification` is supporting to show a cancel button to cancel the
395+
* long running operation.
396+
*
397+
* @return The thenable the task-callback returned.
398+
*/
399+
withProgressCustomIcon<R>(
400+
icon: string,
401+
task: (progress: Progress<{ message?: string; increment?: number }>, token: CancellationToken) => Thenable<R>
402+
): Thenable<R>;
403+
377404
/**
378405
* Create a [TreeView](#TreeView) for the view contributed using the extension point `views`.
379406
* @param viewId Id of the view contributed using the extension point `views`.

src/client/common/constants.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ export namespace Octicons {
6969
export const Test_Fail = '$(alert)';
7070
export const Test_Error = '$(x)';
7171
export const Test_Skip = '$(circle-slash)';
72+
export const Downloading = '$(cloud-download)';
73+
export const Installing = '$(desktop-download)';
7274
}
7375

7476
export const Button_Text_Tests_View_Output = 'View Output';

src/client/common/net/fileDownloader.ts

Lines changed: 32 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55

66
import { inject, injectable } from 'inversify';
77
import * as requestTypes from 'request';
8-
import { Progress, ProgressLocation } from 'vscode';
8+
import { Progress } from 'vscode';
99
import { IApplicationShell } from '../application/types';
10+
import { Octicons } from '../constants';
1011
import { IFileSystem, WriteStream } from '../platform/types';
1112
import { DownloadOptions, IFileDownloader, IHttpClient } from '../types';
1213
import { Http } from '../utils/localize';
@@ -40,12 +41,13 @@ export class FileDownloader implements IFileDownloader {
4041
progressMessage: string,
4142
tmpFilePath: string
4243
): Promise<void> {
43-
await this.appShell.withProgress({ location: ProgressLocation.Window }, async (progress) => {
44+
await this.appShell.withProgressCustomIcon(Octicons.Downloading, async (progress) => {
4445
const req = await this.httpClient.downloadFile(uri);
4546
const fileStream = this.fs.createWriteStream(tmpFilePath);
4647
return this.displayDownloadProgress(uri, progress, req, fileStream, progressMessage);
4748
});
4849
}
50+
4951
public async displayDownloadProgress(
5052
uri: string,
5153
progress: Progress<{ message?: string; increment?: number }>,
@@ -64,17 +66,8 @@ export class FileDownloader implements IFileDownloader {
6466
// tslint:disable-next-line: no-require-imports
6567
const requestProgress = require('request-progress');
6668
requestProgress(request)
67-
// tslint:disable-next-line: no-any
68-
.on('progress', (state: any) => {
69-
const received = Math.round(state.size.transferred / 1024);
70-
const total = Math.round(state.size.total / 1024);
71-
const percentage = Math.round(100 * state.percent);
72-
const message = Http.downloadingFileProgress().format(
73-
progressMessagePrefix,
74-
received.toString(),
75-
total.toString(),
76-
percentage.toString()
77-
);
69+
.on('progress', (state: RequestProgressState) => {
70+
const message = formatProgressMessageWithState(progressMessagePrefix, state);
7871
progress.report({ message });
7972
})
8073
// Handle errors from download.
@@ -86,3 +79,29 @@ export class FileDownloader implements IFileDownloader {
8679
});
8780
}
8881
}
82+
83+
type RequestProgressState = {
84+
percent: number;
85+
speed: number;
86+
size: {
87+
total: number;
88+
transferred: number;
89+
};
90+
time: {
91+
elapsed: number;
92+
remaining: number;
93+
};
94+
};
95+
96+
function formatProgressMessageWithState(progressMessagePrefix: string, state: RequestProgressState): string {
97+
const received = Math.round(state.size.transferred / 1024);
98+
const total = Math.round(state.size.total / 1024);
99+
const percentage = Math.round(100 * state.percent);
100+
101+
return Http.downloadingFileProgress().format(
102+
progressMessagePrefix,
103+
received.toString(),
104+
total.toString(),
105+
percentage.toString()
106+
);
107+
}

src/test/common/net/fileDownloader.unit.test.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ suite('File Downloader', () => {
111111
const progressReportStub = sinon.stub();
112112
const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub };
113113
const tmpFilePath = await fs.createTemporaryFile('.json');
114-
when(appShell.withProgress(anything(), anything())).thenCall((_, cb) => cb(progressReporter));
114+
when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter));
115115

116116
fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell));
117117
await fileDownloader.downloadFileWithStatusBarProgress(uri, 'hello', tmpFilePath.filePath);
@@ -125,7 +125,7 @@ suite('File Downloader', () => {
125125
nock('https://python.extension').get('/package.json').reply(500);
126126
const progressReportStub = sinon.stub();
127127
const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub };
128-
when(appShell.withProgress(anything(), anything())).thenCall((_, cb) => cb(progressReporter));
128+
when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter));
129129
const tmpFilePath = await fs.createTemporaryFile('.json');
130130

131131
fileDownloader = new FileDownloader(instance(httpClient), fs, instance(appShell));
@@ -142,7 +142,7 @@ suite('File Downloader', () => {
142142
.reply(200, () => fsExtra.createReadStream(packageJsonFile));
143143
const progressReportStub = sinon.stub();
144144
const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub };
145-
when(appShell.withProgress(anything(), anything())).thenCall((_, cb) => cb(progressReporter));
145+
when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter));
146146

147147
// Use bogus files that cannot be created (on windows, invalid drives, on mac & linux use invalid home directories).
148148
const invalidFileName = new PlatformService().isWindows
@@ -161,7 +161,7 @@ suite('File Downloader', () => {
161161
.reply(200, () => fsExtra.createReadStream(packageJsonFile));
162162
const progressReportStub = sinon.stub();
163163
const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub };
164-
when(appShell.withProgress(anything(), anything())).thenCall((_, cb) => cb(progressReporter));
164+
when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter));
165165
// Create a file stream that will throw an error when written to (use ErroringMemoryStream).
166166
const tmpFilePath = 'bogus file';
167167
const fileSystem = mock(FileSystem);
@@ -188,7 +188,7 @@ suite('File Downloader', () => {
188188
]);
189189
const progressReportStub = sinon.stub();
190190
const progressReporter: Progress<ProgressReporterData> = { report: progressReportStub };
191-
when(appShell.withProgress(anything(), anything())).thenCall((_, cb) => cb(progressReporter));
191+
when(appShell.withProgressCustomIcon(anything(), anything())).thenCall((_, cb) => cb(progressReporter));
192192
const tmpFilePath = await fs.createTemporaryFile('.json');
193193
// Mock request-progress to throttle 1ms, so we can get progress messages.
194194
// I.e. report progress every 1ms. (however since download is delayed to 10ms,

0 commit comments

Comments
 (0)