Reveal the error location after on failed verify.

Closes #608
Closes #229

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta
2022-06-08 11:29:18 +02:00
committed by Akos Kitta
parent a715da3d18
commit d6f4096cd0
23 changed files with 1545 additions and 392 deletions

View File

@@ -0,0 +1,234 @@
import { notEmpty } from '@theia/core';
import { nls } from '@theia/core/lib/common/nls';
import { FileUri } from '@theia/core/lib/node/file-uri';
import {
Location,
Range,
Position,
} from '@theia/core/shared/vscode-languageserver-protocol';
import { Sketch } from '../common/protocol';
export interface ErrorInfo {
readonly message?: string;
readonly location?: Location;
readonly details?: string;
}
export interface ErrorSource {
readonly content: string | ReadonlyArray<Uint8Array>;
readonly sketch?: Sketch;
}
export function tryParseError(source: ErrorSource): ErrorInfo[] {
const { content, sketch } = source;
const err =
typeof content === 'string'
? content
: Buffer.concat(content).toString('utf8');
if (sketch) {
return tryParse(err)
.map(remapErrorMessages)
.filter(isLocationInSketch(sketch))
.map(errorInfo());
}
return [];
}
interface ParseResult {
readonly path: string;
readonly line: number;
readonly column?: number;
readonly errorPrefix: string;
readonly error: string;
readonly message?: string;
}
namespace ParseResult {
export function keyOf(result: ParseResult): string {
/**
* The CLI compiler might return with the same error multiple times. This is the key function for the distinct set calculation.
*/
return JSON.stringify(result);
}
}
function isLocationInSketch(
sketch: Sketch
): (value: ParseResult, index: number, array: ParseResult[]) => unknown {
return (result) => {
const uri = FileUri.create(result.path).toString();
if (!Sketch.isInSketch(uri, sketch)) {
console.warn(
`URI <${uri}> is not contained in sketch: <${JSON.stringify(sketch)}>`
);
return false;
}
return true;
};
}
function errorInfo(): (value: ParseResult) => ErrorInfo {
return ({ error, message, path, line, column }) => ({
message: error,
details: message,
location: {
uri: FileUri.create(path).toString(),
range: range(line, column),
},
});
}
function range(line: number, column?: number): Range {
const start = Position.create(
line - 1,
typeof column === 'number' ? column - 1 : 0
);
return {
start,
end: start,
};
}
export function tryParse(raw: string): ParseResult[] {
// Shamelessly stolen from the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L137
const re = new RegExp(
'(.+\\.\\w+):(\\d+)(:\\d+)*:\\s*((fatal)?\\s*error:\\s*)(.*)\\s*',
'gm'
);
return [
...new Map(
Array.from(raw.matchAll(re) ?? [])
.map((match) => {
const [, path, rawLine, rawColumn, errorPrefix, , error] = match.map(
(match) => (match ? match.trim() : match)
);
const line = Number.parseInt(rawLine, 10);
if (!Number.isInteger(line)) {
console.warn(
`Could not parse line number. Raw input: <${rawLine}>, parsed integer: <${line}>.`
);
return undefined;
}
let column: number | undefined = undefined;
if (rawColumn) {
const normalizedRawColumn = rawColumn.slice(-1); // trims the leading colon => `:3` will be `3`
column = Number.parseInt(normalizedRawColumn, 10);
if (!Number.isInteger(column)) {
console.warn(
`Could not parse column number. Raw input: <${normalizedRawColumn}>, parsed integer: <${column}>.`
);
}
}
return {
path,
line,
column,
errorPrefix,
error,
};
})
.filter(notEmpty)
.map((result) => [ParseResult.keyOf(result), result])
).values(),
];
}
/**
* Converts cryptic and legacy error messages to nice ones. Taken from the Java IDE.
*/
function remapErrorMessages(result: ParseResult): ParseResult {
const knownError = KnownErrors[result.error];
if (!knownError) {
return result;
}
const { message, error } = knownError;
return {
...result,
...(message && { message }),
...(error && { error }),
};
}
// Based on the Java IDE: https://github.com/arduino/Arduino/blob/43b0818f7fa8073301db1b80ac832b7b7596b828/arduino-core/src/cc/arduino/Compiler.java#L528-L578
const KnownErrors: Record<string, { error: string; message?: string }> = {
'SPI.h: No such file or directory': {
error: nls.localize(
'arduino/cli-error-parser/spiError',
'Please import the SPI library from the Sketch > Import Library menu.'
),
message: nls.localize(
'arduino/cli-error-parser/spiMessage',
'As of Arduino 0019, the Ethernet library depends on the SPI library.\nYou appear to be using it or another library that depends on the SPI library.'
),
},
"'BYTE' was not declared in this scope": {
error: nls.localize(
'arduino/cli-error-parser/byteError',
"The 'BYTE' keyword is no longer supported."
),
message: nls.localize(
'arduino/cli-error-parser/byteMessage',
"As of Arduino 1.0, the 'BYTE' keyword is no longer supported.\nPlease use Serial.write() instead."
),
},
"no matching function for call to 'Server::Server(int)'": {
error: nls.localize(
'arduino/cli-error-parser/serverError',
'The Server class has been renamed EthernetServer.'
),
message: nls.localize(
'arduino/cli-error-parser/serverMessage',
'As of Arduino 1.0, the Server class in the Ethernet library has been renamed to EthernetServer.'
),
},
"no matching function for call to 'Client::Client(byte [4], int)'": {
error: nls.localize(
'arduino/cli-error-parser/clientError',
'The Client class has been renamed EthernetClient.'
),
message: nls.localize(
'arduino/cli-error-parser/clientMessage',
'As of Arduino 1.0, the Client class in the Ethernet library has been renamed to EthernetClient.'
),
},
"'Udp' was not declared in this scope": {
error: nls.localize(
'arduino/cli-error-parser/udpError',
'The Udp class has been renamed EthernetUdp.'
),
message: nls.localize(
'arduino/cli-error-parser/udpMessage',
'As of Arduino 1.0, the Udp class in the Ethernet library has been renamed to EthernetUdp.'
),
},
"'class TwoWire' has no member named 'send'": {
error: nls.localize(
'arduino/cli-error-parser/sendError',
'Wire.send() has been renamed Wire.write().'
),
message: nls.localize(
'arduino/cli-error-parser/sendMessage',
'As of Arduino 1.0, the Wire.send() function was renamed to Wire.write() for consistency with other libraries.'
),
},
"'class TwoWire' has no member named 'receive'": {
error: nls.localize(
'arduino/cli-error-parser/receiveError',
'Wire.receive() has been renamed Wire.read().'
),
message: nls.localize(
'arduino/cli-error-parser/receiveMessage',
'As of Arduino 1.0, the Wire.receive() function was renamed to Wire.read() for consistency with other libraries.'
),
},
"'Mouse' was not declared in this scope": {
error: nls.localize(
'arduino/cli-error-parser/mouseError',
"'Mouse' not found. Does your sketch include the line '#include <Mouse.h>'?"
),
},
"'Keyboard' was not declared in this scope": {
error: nls.localize(
'arduino/cli-error-parser/keyboardError',
"'Keyboard' not found. Does your sketch include the line '#include <Keyboard.h>'?"
),
},
};

View File

@@ -4,7 +4,11 @@ import { relative } from 'path';
import * as jspb from 'google-protobuf';
import { BoolValue } from 'google-protobuf/google/protobuf/wrappers_pb';
import { ClientReadableStream } from '@grpc/grpc-js';
import { CompilerWarnings, CoreService } from '../common/protocol/core-service';
import {
CompilerWarnings,
CoreService,
CoreError,
} from '../common/protocol/core-service';
import {
CompileRequest,
CompileResponse,
@@ -19,27 +23,24 @@ import {
UploadUsingProgrammerResponse,
} from './cli-protocol/cc/arduino/cli/commands/v1/upload_pb';
import { ResponseService } from '../common/protocol/response-service';
import { NotificationServiceServer } from '../common/protocol';
import { Board, OutputMessage, Port, Status } from '../common/protocol';
import { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { firstToUpperCase, firstToLowerCase } from '../common/utils';
import { Port } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import { nls } from '@theia/core';
import { Port as GrpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import { ApplicationError, Disposable, nls } from '@theia/core';
import { MonitorManager } from './monitor-manager';
import { SimpleBuffer } from './utils/simple-buffer';
import { tryParseError } from './cli-error-parser';
import { Instance } from './cli-protocol/cc/arduino/cli/commands/v1/common_pb';
import { firstToUpperCase, notEmpty } from '../common/utils';
import { ServiceError } from './service-error';
const FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS = 32;
@injectable()
export class CoreServiceImpl extends CoreClientAware implements CoreService {
@inject(ResponseService)
protected readonly responseService: ResponseService;
@inject(NotificationServiceServer)
protected readonly notificationService: NotificationServiceServer;
private readonly responseService: ResponseService;
@inject(MonitorManager)
protected readonly monitorManager: MonitorManager;
protected uploading = false;
private readonly monitorManager: MonitorManager;
async compile(
options: CoreService.Compile.Options & {
@@ -47,254 +48,298 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
compilerWarnings?: CompilerWarnings;
}
): Promise<void> {
const { sketchUri, board, compilerWarnings } = options;
const sketchPath = FileUri.fsPath(sketchUri);
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const handler = this.createOnDataHandler();
const request = this.compileRequest(options, instance);
return new Promise<void>((resolve, reject) => {
client
.compile(request)
.on('data', handler.onData)
.on('error', (error) => {
if (!ServiceError.is(error)) {
console.error(
'Unexpected error occurred while compiling the sketch.',
error
);
reject(error);
} else {
const compilerErrors = tryParseError({
content: handler.stderr,
sketch: options.sketch,
});
const message = nls.localize(
'arduino/compile/error',
'Compilation error: {0}',
compilerErrors
.map(({ message }) => message)
.filter(notEmpty)
.shift() ?? error.details
);
this.sendResponse(
error.details + '\n\n' + message,
OutputMessage.Severity.Error
);
reject(CoreError.VerifyFailed(message, compilerErrors));
}
})
.on('end', resolve);
}).finally(() => handler.dispose());
}
const compileReq = new CompileRequest();
compileReq.setInstance(instance);
compileReq.setSketchPath(sketchPath);
private compileRequest(
options: CoreService.Compile.Options & {
exportBinaries?: boolean;
compilerWarnings?: CompilerWarnings;
},
instance: Instance
): CompileRequest {
const { sketch, board, compilerWarnings } = options;
const sketchUri = sketch.uri;
const sketchPath = FileUri.fsPath(sketchUri);
const request = new CompileRequest();
request.setInstance(instance);
request.setSketchPath(sketchPath);
if (board?.fqbn) {
compileReq.setFqbn(board.fqbn);
request.setFqbn(board.fqbn);
}
if (compilerWarnings) {
compileReq.setWarnings(compilerWarnings.toLowerCase());
request.setWarnings(compilerWarnings.toLowerCase());
}
compileReq.setOptimizeForDebug(options.optimizeForDebug);
compileReq.setPreprocess(false);
compileReq.setVerbose(options.verbose);
compileReq.setQuiet(false);
request.setOptimizeForDebug(options.optimizeForDebug);
request.setPreprocess(false);
request.setVerbose(options.verbose);
request.setQuiet(false);
if (typeof options.exportBinaries === 'boolean') {
const exportBinaries = new BoolValue();
exportBinaries.setValue(options.exportBinaries);
compileReq.setExportBinaries(exportBinaries);
}
this.mergeSourceOverrides(compileReq, options);
const result = client.compile(compileReq);
const compileBuffer = new SimpleBuffer(
this.flushOutputPanelMessages.bind(this),
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
);
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (cr: CompileResponse) => {
compileBuffer.addChunk(cr.getOutStream_asU8());
compileBuffer.addChunk(cr.getErrStream_asU8());
});
result.on('error', (error) => {
compileBuffer.clearFlushInterval();
reject(error);
});
result.on('end', () => {
compileBuffer.clearFlushInterval();
resolve();
});
});
this.responseService.appendToOutput({
chunk: '\n--------------------------\nCompilation complete.\n',
});
} catch (e) {
const errorMessage = nls.localize(
'arduino/compile/error',
'Compilation error: {0}',
e.details
);
this.responseService.appendToOutput({
chunk: `${errorMessage}\n`,
severity: 'error',
});
throw new Error(errorMessage);
request.setExportBinaries(exportBinaries);
}
this.mergeSourceOverrides(request, options);
return request;
}
async upload(options: CoreService.Upload.Options): Promise<void> {
await this.doUpload(
return this.doUpload(
options,
() => new UploadRequest(),
(client, req) => client.upload(req)
(client, req) => client.upload(req),
(message: string, info: CoreError.ErrorInfo[]) =>
CoreError.UploadFailed(message, info),
'upload'
);
}
async uploadUsingProgrammer(
options: CoreService.Upload.Options
): Promise<void> {
await this.doUpload(
return this.doUpload(
options,
() => new UploadUsingProgrammerRequest(),
(client, req) => client.uploadUsingProgrammer(req),
(message: string, info: CoreError.ErrorInfo[]) =>
CoreError.UploadUsingProgrammerFailed(message, info),
'upload using programmer'
);
}
isUploading(): Promise<boolean> {
return Promise.resolve(this.uploading);
}
protected async doUpload(
options: CoreService.Upload.Options,
requestProvider: () => UploadRequest | UploadUsingProgrammerRequest,
// tslint:disable-next-line:max-line-length
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest,
responseHandler: (
client: ArduinoCoreServiceClient,
req: UploadRequest | UploadUsingProgrammerRequest
request: UploadRequest | UploadUsingProgrammerRequest
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
task = 'upload'
errorHandler: (
message: string,
info: CoreError.ErrorInfo[]
) => ApplicationError<number, CoreError.ErrorInfo[]>,
task: string
): Promise<void> {
await this.compile(Object.assign(options, { exportBinaries: false }));
this.uploading = true;
const { sketchUri, board, port, programmer } = options;
await this.monitorManager.notifyUploadStarted(board, port);
const sketchPath = FileUri.fsPath(sketchUri);
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const request = this.uploadOrUploadUsingProgrammerRequest(
options,
instance,
requestFactory
);
const handler = this.createOnDataHandler();
return this.notifyUploadWillStart(options).then(() =>
new Promise<void>((resolve, reject) => {
responseHandler(client, request)
.on('data', handler.onData)
.on('error', (error) => {
if (!ServiceError.is(error)) {
console.error(`Unexpected error occurred while ${task}.`, error);
reject(error);
} else {
const message = nls.localize(
'arduino/upload/error',
'{0} error: {1}',
firstToUpperCase(task),
error.details
);
this.sendResponse(error.details, OutputMessage.Severity.Error);
reject(
errorHandler(
message,
tryParseError({
content: handler.stderr,
sketch: options.sketch,
})
)
);
}
})
.on('end', resolve);
}).finally(async () => {
handler.dispose();
await this.notifyUploadDidFinish(options);
})
);
}
const req = requestProvider();
req.setInstance(instance);
req.setSketchPath(sketchPath);
private uploadOrUploadUsingProgrammerRequest(
options: CoreService.Upload.Options,
instance: Instance,
requestFactory: () => UploadRequest | UploadUsingProgrammerRequest
): UploadRequest | UploadUsingProgrammerRequest {
const { sketch, board, port, programmer } = options;
const sketchPath = FileUri.fsPath(sketch.uri);
const request = requestFactory();
request.setInstance(instance);
request.setSketchPath(sketchPath);
if (board?.fqbn) {
req.setFqbn(board.fqbn);
request.setFqbn(board.fqbn);
}
const p = new Port();
if (port) {
p.setAddress(port.address);
p.setLabel(port.addressLabel);
p.setProtocol(port.protocol);
p.setProtocolLabel(port.protocolLabel);
}
req.setPort(p);
request.setPort(this.createPort(port));
if (programmer) {
req.setProgrammer(programmer.id);
request.setProgrammer(programmer.id);
}
req.setVerbose(options.verbose);
req.setVerify(options.verify);
request.setVerbose(options.verbose);
request.setVerify(options.verify);
options.userFields.forEach((e) => {
req.getUserFieldsMap().set(e.name, e.value);
request.getUserFieldsMap().set(e.name, e.value);
});
const result = responseHandler(client, req);
const uploadBuffer = new SimpleBuffer(
this.flushOutputPanelMessages.bind(this),
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
);
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (resp: UploadResponse) => {
uploadBuffer.addChunk(resp.getOutStream_asU8());
uploadBuffer.addChunk(resp.getErrStream_asU8());
});
result.on('error', (error) => {
uploadBuffer.clearFlushInterval();
reject(error);
});
result.on('end', () => {
uploadBuffer.clearFlushInterval();
resolve();
});
});
this.responseService.appendToOutput({
chunk:
'\n--------------------------\n' +
firstToLowerCase(task) +
' complete.\n',
});
} catch (e) {
const errorMessage = nls.localize(
'arduino/upload/error',
'{0} error: {1}',
firstToUpperCase(task),
e.details
);
this.responseService.appendToOutput({
chunk: `${errorMessage}\n`,
severity: 'error',
});
throw new Error(errorMessage);
} finally {
this.uploading = false;
this.monitorManager.notifyUploadFinished(board, port);
}
return request;
}
async burnBootloader(options: CoreService.Bootloader.Options): Promise<void> {
this.uploading = true;
const { board, port, programmer } = options;
await this.monitorManager.notifyUploadStarted(board, port);
await this.coreClientProvider.initialized;
const coreClient = await this.coreClient();
const { client, instance } = coreClient;
const burnReq = new BurnBootloaderRequest();
burnReq.setInstance(instance);
if (board?.fqbn) {
burnReq.setFqbn(board.fqbn);
}
const p = new Port();
if (port) {
p.setAddress(port.address);
p.setLabel(port.addressLabel);
p.setProtocol(port.protocol);
p.setProtocolLabel(port.protocolLabel);
}
burnReq.setPort(p);
if (programmer) {
burnReq.setProgrammer(programmer.id);
}
burnReq.setVerify(options.verify);
burnReq.setVerbose(options.verbose);
const result = client.burnBootloader(burnReq);
const bootloaderBuffer = new SimpleBuffer(
this.flushOutputPanelMessages.bind(this),
FLUSH_OUTPUT_MESSAGES_TIMEOUT_MS
const handler = this.createOnDataHandler();
const request = this.burnBootloaderRequest(options, instance);
return this.notifyUploadWillStart(options).then(() =>
new Promise<void>((resolve, reject) => {
client
.burnBootloader(request)
.on('data', handler.onData)
.on('error', (error) => {
if (!ServiceError.is(error)) {
console.error(
'Unexpected error occurred while burning the bootloader.',
error
);
reject(error);
} else {
this.sendResponse(error.details, OutputMessage.Severity.Error);
reject(
CoreError.BurnBootloaderFailed(
nls.localize(
'arduino/burnBootloader/error',
'Error while burning the bootloader: {0}',
error.details
),
tryParseError({ content: handler.stderr })
)
);
}
})
.on('end', resolve);
}).finally(async () => {
handler.dispose();
await this.notifyUploadDidFinish(options);
})
);
try {
await new Promise<void>((resolve, reject) => {
result.on('data', (resp: BurnBootloaderResponse) => {
bootloaderBuffer.addChunk(resp.getOutStream_asU8());
bootloaderBuffer.addChunk(resp.getErrStream_asU8());
});
result.on('error', (error) => {
bootloaderBuffer.clearFlushInterval();
reject(error);
});
result.on('end', () => {
bootloaderBuffer.clearFlushInterval();
resolve();
});
});
} catch (e) {
const errorMessage = nls.localize(
'arduino/burnBootloader/error',
'Error while burning the bootloader: {0}',
e.details
);
this.responseService.appendToOutput({
chunk: `${errorMessage}\n`,
severity: 'error',
});
throw new Error(errorMessage);
} finally {
this.uploading = false;
await this.monitorManager.notifyUploadFinished(board, port);
}
private burnBootloaderRequest(
options: CoreService.Bootloader.Options,
instance: Instance
): BurnBootloaderRequest {
const { board, port, programmer } = options;
const request = new BurnBootloaderRequest();
request.setInstance(instance);
if (board?.fqbn) {
request.setFqbn(board.fqbn);
}
request.setPort(this.createPort(port));
if (programmer) {
request.setProgrammer(programmer.id);
}
request.setVerify(options.verify);
request.setVerbose(options.verbose);
return request;
}
private createOnDataHandler<R extends StreamingResponse>(): Disposable & {
stderr: Buffer[];
onData: (response: R) => void;
} {
const stderr: Buffer[] = [];
const buffer = new SimpleBuffer((chunks) => {
Array.from(chunks.entries()).forEach(([severity, chunk]) => {
if (chunk) {
this.sendResponse(chunk, severity);
}
});
});
const onData = StreamingResponse.createOnDataHandler(stderr, (out, err) => {
buffer.addChunk(out);
buffer.addChunk(err, OutputMessage.Severity.Error);
});
return {
dispose: () => buffer.dispose(),
stderr,
onData,
};
}
private sendResponse(
chunk: string,
severity: OutputMessage.Severity = OutputMessage.Severity.Info
): void {
this.responseService.appendToOutput({ chunk, severity });
}
private async notifyUploadWillStart({
board,
port,
}: {
board?: Board | undefined;
port?: Port | undefined;
}): Promise<void> {
return this.monitorManager.notifyUploadStarted(board, port);
}
private async notifyUploadDidFinish({
board,
port,
}: {
board?: Board | undefined;
port?: Port | undefined;
}): Promise<Status> {
return this.monitorManager.notifyUploadFinished(board, port);
}
private mergeSourceOverrides(
req: { getSourceOverrideMap(): jspb.Map<string, string> },
options: CoreService.Compile.Options
): void {
const sketchPath = FileUri.fsPath(options.sketchUri);
const sketchPath = FileUri.fsPath(options.sketch.uri);
for (const uri of Object.keys(options.sourceOverride)) {
const content = options.sourceOverride[uri];
if (content) {
@@ -304,9 +349,33 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
}
}
private flushOutputPanelMessages(chunk: string): void {
this.responseService.appendToOutput({
chunk,
});
private createPort(port: Port | undefined): GrpcPort {
const grpcPort = new GrpcPort();
if (port) {
grpcPort.setAddress(port.address);
grpcPort.setLabel(port.addressLabel);
grpcPort.setProtocol(port.protocol);
grpcPort.setProtocolLabel(port.protocolLabel);
}
return grpcPort;
}
}
type StreamingResponse =
| CompileResponse
| UploadResponse
| UploadUsingProgrammerResponse
| BurnBootloaderResponse;
namespace StreamingResponse {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createOnDataHandler<R extends StreamingResponse>(
stderr: Uint8Array[],
onData: (out: Uint8Array, err: Uint8Array) => void
): (response: R) => void {
return (response: R) => {
const out = response.getOutStream_asU8();
const err = response.getErrStream_asU8();
stderr.push(err);
onData(out, err);
};
}
}

View File

@@ -3,23 +3,19 @@ import {
injectable,
postConstruct,
} from '@theia/core/shared/inversify';
import { join, basename } from 'path';
import { join } from 'path';
import * as fs from 'fs';
import { promisify } from 'util';
import { FileUri } from '@theia/core/lib/node/file-uri';
import {
Sketch,
SketchRef,
SketchContainer,
} from '../common/protocol/sketches-service';
import { SketchesServiceImpl } from './sketches-service-impl';
import { ExamplesService } from '../common/protocol/examples-service';
import {
LibraryLocation,
LibraryPackage,
LibraryService,
} from '../common/protocol';
import { ConfigServiceImpl } from './config-service-impl';
import { duration } from '../common/decorators';
import { URI } from '@theia/core/lib/common/uri';
import { Path } from '@theia/core/lib/common/path';
@@ -88,14 +84,8 @@ export class BuiltInExamplesServiceImpl {
@injectable()
export class ExamplesServiceImpl implements ExamplesService {
@inject(SketchesServiceImpl)
protected readonly sketchesService: SketchesServiceImpl;
@inject(LibraryService)
protected readonly libraryService: LibraryService;
@inject(ConfigServiceImpl)
protected readonly configService: ConfigServiceImpl;
private readonly libraryService: LibraryService;
@inject(BuiltInExamplesServiceImpl)
private readonly builtInExamplesService: BuiltInExamplesServiceImpl;
@@ -117,7 +107,7 @@ export class ExamplesServiceImpl implements ExamplesService {
fqbn,
});
for (const pkg of packages) {
const container = await this.tryGroupExamplesNew(pkg);
const container = await this.tryGroupExamples(pkg);
const { location } = pkg;
if (location === LibraryLocation.USER) {
user.push(container);
@@ -130,9 +120,6 @@ export class ExamplesServiceImpl implements ExamplesService {
any.push(container);
}
}
// user.sort((left, right) => left.label.localeCompare(right.label));
// current.sort((left, right) => left.label.localeCompare(right.label));
// any.sort((left, right) => left.label.localeCompare(right.label));
return { user, current, any };
}
@@ -141,7 +128,7 @@ export class ExamplesServiceImpl implements ExamplesService {
* folder hierarchy. This method tries to workaround it by falling back to the `installDirUri` and manually creating the
* location of the examples. Otherwise it creates the example container from the direct examples FS paths.
*/
protected async tryGroupExamplesNew({
private async tryGroupExamples({
label,
exampleUris,
installDirUri,
@@ -208,10 +195,6 @@ export class ExamplesServiceImpl implements ExamplesService {
if (!child) {
child = SketchContainer.create(label);
parent.children.push(child);
//TODO: remove or move sort
parent.children.sort((left, right) =>
left.label.localeCompare(right.label)
);
}
return child;
};
@@ -230,65 +213,7 @@ export class ExamplesServiceImpl implements ExamplesService {
container
);
refContainer.sketches.push(ref);
//TODO: remove or move sort
refContainer.sketches.sort((left, right) =>
left.name.localeCompare(right.name)
);
}
return container;
}
// Built-ins are included inside the IDE.
protected async load(path: string): Promise<SketchContainer> {
if (!(await promisify(fs.exists)(path))) {
throw new Error('Examples are not available');
}
const stat = await promisify(fs.stat)(path);
if (!stat.isDirectory) {
throw new Error(`${path} is not a directory.`);
}
const names = await promisify(fs.readdir)(path);
const sketches: SketchRef[] = [];
const children: SketchContainer[] = [];
for (const p of names.map((name) => join(path, name))) {
const stat = await promisify(fs.stat)(p);
if (stat.isDirectory()) {
const sketch = await this.tryLoadSketch(p);
if (sketch) {
sketches.push({ name: sketch.name, uri: sketch.uri });
sketches.sort((left, right) => left.name.localeCompare(right.name));
} else {
const child = await this.load(p);
children.push(child);
children.sort((left, right) => left.label.localeCompare(right.label));
}
}
}
const label = basename(path);
return {
label,
children,
sketches,
};
}
protected async group(paths: string[]): Promise<Map<string, fs.Stats>> {
const map = new Map<string, fs.Stats>();
for (const path of paths) {
const stat = await promisify(fs.stat)(path);
map.set(path, stat);
}
return map;
}
protected async tryLoadSketch(path: string): Promise<Sketch | undefined> {
try {
const sketch = await this.sketchesService.loadSketch(
FileUri.create(path).toString()
);
return sketch;
} catch {
return undefined;
}
}
}

View File

@@ -0,0 +1,23 @@
import { Metadata, StatusObject } from '@grpc/grpc-js';
export type ServiceError = StatusObject & Error;
export namespace ServiceError {
export function is(arg: unknown): arg is ServiceError {
return arg instanceof Error && isStatusObjet(arg);
}
function isStatusObjet(arg: unknown): arg is StatusObject {
if (typeof arg === 'object') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const any = arg as any;
return (
!!arg &&
'code' in arg &&
'details' in arg &&
typeof any.details === 'string' &&
'metadata' in arg &&
any.metadata instanceof Metadata
);
}
return false;
}
}

View File

@@ -1,17 +1,19 @@
export class SimpleBuffer {
private chunks: Uint8Array[] = [];
import { Disposable } from '@theia/core/shared/vscode-languageserver-protocol';
import { OutputMessage } from '../../common/protocol';
const DEFAULT_FLUS_TIMEOUT_MS = 32;
export class SimpleBuffer implements Disposable {
private readonly flush: () => void;
private readonly chunks = Chunks.create();
private flushInterval?: NodeJS.Timeout;
private flush: () => void;
constructor(onFlush: (chunk: string) => void, flushTimeout: number) {
const flush = () => {
if (this.chunks.length > 0) {
const chunkString = Buffer.concat(this.chunks).toString();
this.clearChunks();
onFlush(chunkString);
onFlush(chunks);
}
};
@@ -19,12 +21,15 @@ export class SimpleBuffer {
this.flushInterval = setInterval(flush, flushTimeout);
}
public addChunk(chunk: Uint8Array): void {
this.chunks.push(chunk);
public addChunk(
chunk: Uint8Array,
severity: OutputMessage.Severity = OutputMessage.Severity.Info
): void {
this.chunks.get(severity)?.push(chunk);
}
private clearChunks(): void {
this.chunks = [];
Chunks.clear(this.chunks);
}
public clearFlushInterval(): void {
@@ -32,6 +37,37 @@ export class SimpleBuffer {
this.clearChunks();
clearInterval(this.flushInterval);
this.clearChunks();
this.flushInterval = undefined;
}
}
type Chunks = Map<OutputMessage.Severity, Uint8Array[]>;
namespace Chunks {
export function create(): Chunks {
return new Map([
[OutputMessage.Severity.Error, []],
[OutputMessage.Severity.Warning, []],
[OutputMessage.Severity.Info, []],
]);
}
export function clear(chunks: Chunks): Chunks {
for (const chunk of chunks.values()) {
chunk.length = 0;
}
return chunks;
}
export function isEmpty(chunks: Chunks): boolean {
return ![...chunks.values()].some((chunk) => Boolean(chunk.length));
}
export function toString(
chunks: Chunks
): Map<OutputMessage.Severity, string | undefined> {
return new Map(
Array.from(chunks.entries()).map(([severity, buffers]) => [
severity,
buffers.length ? Buffer.concat(buffers).toString() : undefined,
])
);
}
}