Show 'progress' indicator during verify/upload.

Closes #575
Closes #1175

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta
2022-07-27 18:04:40 +02:00
committed by Akos Kitta
parent 27a2a6ca03
commit e156dcc213
13 changed files with 554 additions and 407 deletions

View File

@@ -1,7 +1,6 @@
import { ApplicationError } from '@theia/core/lib/common/application-error';
import type { Location } from '@theia/core/shared/vscode-languageserver-protocol';
import type {
Board,
BoardUserField,
Port,
} from '../../common/protocol/boards-service';
@@ -60,46 +59,39 @@ export namespace CoreError {
export const CoreServicePath = '/services/core-service';
export const CoreService = Symbol('CoreService');
export interface CoreService {
compile(
options: CoreService.Compile.Options &
Readonly<{
exportBinaries?: boolean;
compilerWarnings?: CompilerWarnings;
}>
): Promise<void>;
upload(options: CoreService.Upload.Options): Promise<void>;
uploadUsingProgrammer(options: CoreService.Upload.Options): Promise<void>;
burnBootloader(options: CoreService.Bootloader.Options): Promise<void>;
compile(options: CoreService.Options.Compile): Promise<void>;
upload(options: CoreService.Options.Upload): Promise<void>;
burnBootloader(options: CoreService.Options.Bootloader): Promise<void>;
}
export namespace CoreService {
export namespace Compile {
export interface Options {
export namespace Options {
export interface Base {
readonly fqbn?: string | undefined;
readonly verbose: boolean; // TODO: (API) why not optional with a default false?
readonly progressId?: string;
}
export interface SketchBased {
readonly sketch: Sketch;
readonly board?: Board;
readonly optimizeForDebug: boolean;
readonly verbose: boolean;
readonly sourceOverride: Record<string, string>;
}
}
export namespace Upload {
export interface Options extends Omit<Compile.Options, 'verbose'> {
export interface BoardBased {
readonly port?: Port;
readonly programmer?: Programmer | undefined;
readonly verify: boolean;
/**
* For the _Verify after upload_ setting.
*/
readonly verify: boolean; // TODO: (API) why not optional with false as the default value?
}
export interface Compile extends Base, SketchBased {
readonly optimizeForDebug: boolean; // TODO: (API) make this optional
readonly sourceOverride: Record<string, string>; // TODO: (API) make this optional
readonly exportBinaries?: boolean;
readonly compilerWarnings?: CompilerWarnings;
}
export interface Upload extends Base, SketchBased, BoardBased {
readonly userFields: BoardUserField[];
readonly verbose: { compile: boolean; upload: boolean };
}
}
export namespace Bootloader {
export interface Options {
readonly board?: Board;
readonly port?: Port;
readonly programmer?: Programmer | undefined;
readonly verbose: boolean;
readonly verify: boolean;
readonly usingProgrammer?: boolean;
}
export interface Bootloader extends Base, BoardBased {}
}
}

View File

@@ -1,10 +1,6 @@
import * as semver from 'semver';
import type { Progress } from '@theia/core/lib/common/message-service-protocol';
import {
CancellationToken,
CancellationTokenSource,
} from '@theia/core/lib/common/cancellation';
import { naturalCompare } from './../utils';
import { ExecuteWithProgress } from './progressible';
import { naturalCompare } from '../utils';
import type { ArduinoComponent } from './arduino-component';
import type { MessageService } from '@theia/core/lib/common/message-service';
import type { ResponseServiceClient } from './response-service';
@@ -32,7 +28,7 @@ export namespace Installable {
/**
* Most recent version comes first, then the previous versions. (`1.8.1`, `1.6.3`, `1.6.2`, `1.6.1` and so on.)
*/
export const COMPARATOR = (left: Version, right: Version) => {
export const COMPARATOR = (left: Version, right: Version): number => {
if (semver.valid(left) && semver.valid(right)) {
return semver.compare(left, right);
}
@@ -50,7 +46,7 @@ export namespace Installable {
version: Installable.Version;
}): Promise<void> {
const { item, version } = options;
return doWithProgress({
return ExecuteWithProgress.doWithProgress({
...options,
progressText: `Processing ${item.name}:${version}`,
run: ({ progressId }) =>
@@ -71,7 +67,7 @@ export namespace Installable {
item: T;
}): Promise<void> {
const { item } = options;
return doWithProgress({
return ExecuteWithProgress.doWithProgress({
...options,
progressText: `Processing ${item.name}${
item.installedVersion ? `:${item.installedVersion}` : ''
@@ -83,51 +79,4 @@ export namespace Installable {
}),
});
}
export async function doWithProgress(options: {
run: ({ progressId }: { progressId: string }) => Promise<void>;
messageService: MessageService;
responseService: ResponseServiceClient;
progressText: string;
}): Promise<void> {
return withProgress(
options.progressText,
options.messageService,
async (progress, _) => {
const progressId = progress.id;
const toDispose = options.responseService.onProgressDidChange(
(progressMessage) => {
if (progressId === progressMessage.progressId) {
const { message, work } = progressMessage;
progress.report({ message, work });
}
}
);
try {
options.responseService.clearOutput();
await options.run({ progressId });
} finally {
toDispose.dispose();
}
}
);
}
async function withProgress(
text: string,
messageService: MessageService,
cb: (progress: Progress, token: CancellationToken) => Promise<void>
): Promise<void> {
const cancellationSource = new CancellationTokenSource();
const { token } = cancellationSource;
const progress = await messageService.showProgress(
{ text, options: { cancelable: false } },
() => cancellationSource.cancel()
);
try {
await cb(progress, token);
} finally {
progress.cancel();
}
}
}

View File

@@ -0,0 +1,60 @@
import type { CancellationToken } from '@theia/core/lib/common/cancellation';
import { CancellationTokenSource } from '@theia/core/lib/common/cancellation';
import type { MessageService } from '@theia/core/lib/common/message-service';
import type { Progress } from '@theia/core/lib/common/message-service-protocol';
import type { ResponseServiceClient } from './response-service';
export namespace ExecuteWithProgress {
export async function doWithProgress<T>(options: {
run: ({ progressId }: { progressId: string }) => Promise<T>;
messageService: MessageService;
responseService: ResponseServiceClient;
progressText: string;
keepOutput?: boolean;
}): Promise<T> {
return withProgress(
options.progressText,
options.messageService,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async (progress, _token) => {
const progressId = progress.id;
const toDispose = options.responseService.onProgressDidChange(
(progressMessage) => {
if (progressId === progressMessage.progressId) {
const { message, work } = progressMessage;
progress.report({ message, work });
}
}
);
try {
if (!options.keepOutput) {
options.responseService.clearOutput();
}
const result = await options.run({ progressId });
return result;
} finally {
toDispose.dispose();
}
}
);
}
async function withProgress<T>(
text: string,
messageService: MessageService,
cb: (progress: Progress, token: CancellationToken) => Promise<T>
): Promise<T> {
const cancellationSource = new CancellationTokenSource();
const { token } = cancellationSource;
const progress = await messageService.showProgress(
{ text, options: { cancelable: false } },
() => cancellationSource.cancel()
);
try {
const result = await cb(progress, token);
return result;
} finally {
progress.cancel();
}
}
}

View File

@@ -46,5 +46,5 @@ export interface ResponseService {
export const ResponseServiceClient = Symbol('ResponseServiceClient');
export interface ResponseServiceClient extends ResponseService {
onProgressDidChange: Event<ProgressMessage>;
clearOutput: () => void;
clearOutput: () => void; // TODO: this should not belong here.
}