#714: Use the build cache to speed up the LS (#1107)

* Notify the LS about the new `build_path` after verify.

Closes #714

Signed-off-by: Akos Kitta <a.kitta@arduino.cc>
This commit is contained in:
Akos Kitta 2022-07-18 10:19:00 +02:00 committed by GitHub
parent ed41b25889
commit 57841b3c0a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 323 additions and 212 deletions

View File

@ -165,7 +165,7 @@
"version": "14.0.0" "version": "14.0.0"
}, },
"languageServer": { "languageServer": {
"version": "0.6.0" "version": "0.7.1"
} }
} }
} }

View File

@ -1,14 +1,12 @@
// @ts-check // @ts-check
(async () => { (async () => {
const fs = require('fs');
const path = require('path'); const path = require('path');
const temp = require('temp');
const shell = require('shelljs'); const shell = require('shelljs');
const semver = require('semver'); const semver = require('semver');
const moment = require('moment'); const moment = require('moment');
const downloader = require('./downloader'); const downloader = require('./downloader');
const { goBuildFromGit } = require('./utils');
const version = (() => { const version = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json')); const pkg = require(path.join(__dirname, '..', 'package.json'));
@ -43,17 +41,24 @@
if (typeof version === 'string') { if (typeof version === 'string') {
const suffix = (() => { const suffix = (() => {
switch (platform) { switch (platform) {
case 'darwin': return 'macOS_64bit.tar.gz'; case 'darwin':
case 'win32': return 'Windows_64bit.zip'; return 'macOS_64bit.tar.gz';
case 'win32':
return 'Windows_64bit.zip';
case 'linux': { case 'linux': {
switch (arch) { switch (arch) {
case 'arm': return 'Linux_ARMv7.tar.gz'; case 'arm':
case 'arm64': return 'Linux_ARM64.tar.gz'; return 'Linux_ARMv7.tar.gz';
case 'x64': return 'Linux_64bit.tar.gz'; case 'arm64':
default: return undefined; return 'Linux_ARM64.tar.gz';
case 'x64':
return 'Linux_64bit.tar.gz';
default:
return undefined;
} }
} }
default: return undefined; default:
return undefined;
} }
})(); })();
if (!suffix) { if (!suffix) {
@ -62,80 +67,21 @@
} }
if (semver.valid(version)) { if (semver.valid(version)) {
const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_${suffix}`; const url = `https://downloads.arduino.cc/arduino-cli/arduino-cli_${version}_${suffix}`;
shell.echo(`📦 Identified released version of the CLI. Downloading version ${version} from '${url}'`); shell.echo(
`📦 Identified released version of the CLI. Downloading version ${version} from '${url}'`
);
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli'); await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
} else if (moment(version, 'YYYYMMDD', true).isValid()) { } else if (moment(version, 'YYYYMMDD', true).isValid()) {
const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`; const url = `https://downloads.arduino.cc/arduino-cli/nightly/arduino-cli_nightly-${version}_${suffix}`;
shell.echo(`🌙 Identified nightly version of the CLI. Downloading version ${version} from '${url}'`); shell.echo(
`🌙 Identified nightly version of the CLI. Downloading version ${version} from '${url}'`
);
await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli'); await downloader.downloadUnzipFile(url, destinationPath, 'arduino-cli');
} else { } else {
shell.echo(`🔥 Could not interpret 'version': ${version}`); shell.echo(`🔥 Could not interpret 'version': ${version}`);
shell.exit(1); shell.exit(1);
} }
} else { } else {
goBuildFromGit(version, destinationPath, 'CLI');
// We assume an object with `owner`, `repo`, commitish?` properties.
const { owner, repo, commitish } = version;
if (!owner) {
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
shell.exit(1);
} }
if (!repo) {
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
shell.exit(1);
}
const url = `https://github.com/${owner}/${repo}.git`;
shell.echo(`Building CLI from ${url}. Commitish: ${commitish ? commitish : 'HEAD'}`);
if (fs.existsSync(destinationPath)) {
shell.echo(`Skipping the CLI build because it already exists: ${destinationPath}`);
return;
}
if (shell.mkdir('-p', buildFolder).code !== 0) {
shell.echo('Could not create build folder.');
shell.exit(1);
}
const tempRepoPath = temp.mkdirSync();
shell.echo(`>>> Cloning CLI source to ${tempRepoPath}...`);
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
shell.exit(1);
}
shell.echo('<<< Cloned CLI repo.')
if (commitish) {
shell.echo(`>>> Checking out ${commitish}...`);
if (shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out ${commitish}.`);
}
shell.echo(`>>> Building the CLI...`);
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
shell.exit(1);
}
shell.echo('<<< CLI build done.')
if (!fs.existsSync(path.join(tempRepoPath, cliName))) {
shell.echo(`Could not find the CLI at ${path.join(tempRepoPath, cliName)}.`);
shell.exit(1);
}
const builtCliPath = path.join(tempRepoPath, cliName);
shell.echo(`>>> Copying CLI from ${builtCliPath} to ${destinationPath}...`);
if (shell.cp(builtCliPath, destinationPath).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Copied the CLI.`);
shell.echo('<<< Verifying CLI...');
if (!fs.existsSync(destinationPath)) {
shell.exit(1);
}
shell.echo('>>> Verified CLI.');
}
})(); })();

View File

@ -7,22 +7,23 @@
const path = require('path'); const path = require('path');
const shell = require('shelljs'); const shell = require('shelljs');
const downloader = require('./downloader'); const downloader = require('./downloader');
const { goBuildFromGit } = require('./utils');
const [DEFAULT_ALS_VERSION, DEFAULT_CLANGD_VERSION] = (() => { const [DEFAULT_LS_VERSION, DEFAULT_CLANGD_VERSION] = (() => {
const pkg = require(path.join(__dirname, '..', 'package.json')); const pkg = require(path.join(__dirname, '..', 'package.json'));
if (!pkg) return undefined; if (!pkg) return [undefined, undefined];
const { arduino } = pkg; const { arduino } = pkg;
if (!arduino) return undefined; if (!arduino) return [undefined, undefined];
const { languageServer, clangd } = arduino; const { languageServer, clangd } = arduino;
if (!languageServer) return undefined; if (!languageServer) return [undefined, undefined];
if (!clangd) return undefined; if (!clangd) return [undefined, undefined];
return [languageServer.version, clangd.version]; return [languageServer.version, clangd.version];
})(); })();
if (!DEFAULT_ALS_VERSION) { if (!DEFAULT_LS_VERSION) {
shell.echo( shell.echo(
`Could not retrieve Arduino Language Server version info from the 'package.json'.` `Could not retrieve Arduino Language Server version info from the 'package.json'.`
); );
@ -39,8 +40,8 @@
const yargs = require('yargs') const yargs = require('yargs')
.option('ls-version', { .option('ls-version', {
alias: 'lv', alias: 'lv',
default: DEFAULT_ALS_VERSION, default: DEFAULT_LS_VERSION,
describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_ALS_VERSION}.`, describe: `The version of the 'arduino-language-server' to download. Defaults to ${DEFAULT_LS_VERSION}.`,
}) })
.option('clangd-version', { .option('clangd-version', {
alias: 'cv', alias: 'cv',
@ -56,7 +57,7 @@
.version(false) .version(false)
.parse(); .parse();
const alsVersion = yargs['ls-version']; const lsVersion = yargs['ls-version'];
const clangdVersion = yargs['clangd-version']; const clangdVersion = yargs['clangd-version'];
const force = yargs['force-download']; const force = yargs['force-download'];
const { platform, arch } = process; const { platform, arch } = process;
@ -87,6 +88,8 @@
lsSuffix = 'Windows_64bit.zip'; lsSuffix = 'Windows_64bit.zip';
clangdSuffix = 'Windows_64bit'; clangdSuffix = 'Windows_64bit';
break; break;
default:
throw new Error(`Unsupported platform/arch: ${platformArch}.`);
} }
if (!lsSuffix || !clangdSuffix) { if (!lsSuffix || !clangdSuffix) {
shell.echo( shell.echo(
@ -95,12 +98,16 @@
shell.exit(1); shell.exit(1);
} }
const alsUrl = `https://downloads.arduino.cc/arduino-language-server/${ if (typeof lsVersion === 'string') {
alsVersion === 'nightly' const lsUrl = `https://downloads.arduino.cc/arduino-language-server/${
lsVersion === 'nightly'
? 'nightly/arduino-language-server' ? 'nightly/arduino-language-server'
: 'arduino-language-server_' + alsVersion : 'arduino-language-server_' + lsVersion
}_${lsSuffix}`; }_${lsSuffix}`;
downloader.downloadUnzipAll(alsUrl, build, lsExecutablePath, force); downloader.downloadUnzipAll(lsUrl, build, lsExecutablePath, force);
} else {
goBuildFromGit(lsVersion, lsExecutablePath, 'language-server');
}
const clangdUrl = `https://downloads.arduino.cc/tools/clangd_${clangdVersion}_${clangdSuffix}.tar.bz2`; const clangdUrl = `https://downloads.arduino.cc/tools/clangd_${clangdVersion}_${clangdSuffix}.tar.bz2`;
downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, { downloader.downloadUnzipAll(clangdUrl, build, clangdExecutablePath, force, {

View File

@ -86,6 +86,7 @@ exports.downloadUnzipFile = async (
* @param targetDir {string} Directory into which to decompress the archive * @param targetDir {string} Directory into which to decompress the archive
* @param targetFile {string} Path to the main file expected after decompressing * @param targetFile {string} Path to the main file expected after decompressing
* @param force {boolean} Whether to download even if the target file exists * @param force {boolean} Whether to download even if the target file exists
* @param decompressOptions {import('decompress').DecompressOptions}
*/ */
exports.downloadUnzipAll = async ( exports.downloadUnzipAll = async (
url, url,

View File

@ -0,0 +1,92 @@
/**
* Clones something from GitHub and builds it with `Golang`.
*
* @param version {object} the version object.
* @param destinationPath {string} the absolute path of the output binary. For example, `C:\\folder\\arduino-cli.exe` or `/path/to/arduino-language-server`
* @param taskName {string} for the CLI logging . Can be `'CLI'` or `'language-server'`, etc.
*/
exports.goBuildFromGit = (version, destinationPath, taskName) => {
const fs = require('fs');
const path = require('path');
const temp = require('temp');
const shell = require('shelljs');
// We assume an object with `owner`, `repo`, commitish?` properties.
if (typeof version !== 'object') {
shell.echo(
`Expected a \`{ owner, repo, commitish }\` object. Got <${version}> instead.`
);
}
const { owner, repo, commitish } = version;
if (!owner) {
shell.echo(`Could not retrieve 'owner' from ${JSON.stringify(version)}`);
shell.exit(1);
}
if (!repo) {
shell.echo(`Could not retrieve 'repo' from ${JSON.stringify(version)}`);
shell.exit(1);
}
const url = `https://github.com/${owner}/${repo}.git`;
shell.echo(
`Building ${taskName} from ${url}. Commitish: ${
commitish ? commitish : 'HEAD'
}`
);
if (fs.existsSync(destinationPath)) {
shell.echo(
`Skipping the ${taskName} build because it already exists: ${destinationPath}`
);
return;
}
const buildFolder = path.join(__dirname, '..', 'build');
if (shell.mkdir('-p', buildFolder).code !== 0) {
shell.echo('Could not create build folder.');
shell.exit(1);
}
const tempRepoPath = temp.mkdirSync();
shell.echo(`>>> Cloning ${taskName} source to ${tempRepoPath}...`);
if (shell.exec(`git clone ${url} ${tempRepoPath}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Cloned ${taskName} repo.`);
if (commitish) {
shell.echo(`>>> Checking out ${commitish}...`);
if (shell.exec(`git -C ${tempRepoPath} checkout ${commitish}`).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Checked out ${commitish}.`);
}
shell.echo(`>>> Building the ${taskName}...`);
if (shell.exec('go build', { cwd: tempRepoPath }).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Done ${taskName} build.`);
const binName = path.basename(destinationPath);
if (!fs.existsSync(path.join(tempRepoPath, binName))) {
shell.echo(
`Could not find the ${taskName} at ${path.join(tempRepoPath, binName)}.`
);
shell.exit(1);
}
const binPath = path.join(tempRepoPath, binName);
shell.echo(
`>>> Copying ${taskName} from ${binPath} to ${destinationPath}...`
);
if (shell.cp(binPath, destinationPath).code !== 0) {
shell.exit(1);
}
shell.echo(`<<< Copied the ${taskName}.`);
shell.echo(`<<< Verifying ${taskName}...`);
if (!fs.existsSync(destinationPath)) {
shell.exit(1);
}
shell.echo(`>>> Verified ${taskName}.`);
};

View File

@ -251,11 +251,14 @@ export class ArduinoFrontendContribution
); );
}); });
const start = async ({ selectedBoard }: BoardsConfig.Config) => { const start = async (
{ selectedBoard }: BoardsConfig.Config,
forceStart = false
) => {
if (selectedBoard) { if (selectedBoard) {
const { name, fqbn } = selectedBoard; const { name, fqbn } = selectedBoard;
if (fqbn) { if (fqbn) {
this.startLanguageServer(fqbn, name); this.startLanguageServer(fqbn, name, forceStart);
} }
} }
}; };
@ -270,7 +273,8 @@ export class ArduinoFrontendContribution
if (event.newValue !== event.oldValue) { if (event.newValue !== event.oldValue) {
switch (event.preferenceName) { switch (event.preferenceName) {
case 'arduino.language.log': case 'arduino.language.log':
start(this.boardsServiceClientImpl.boardsConfig); case 'arduino.language.realTimeDiagnostics':
start(this.boardsServiceClientImpl.boardsConfig, true);
break; break;
case 'arduino.window.zoomLevel': case 'arduino.window.zoomLevel':
if (typeof event.newValue === 'number') { if (typeof event.newValue === 'number') {
@ -318,7 +322,8 @@ export class ArduinoFrontendContribution
protected languageServerStartMutex = new Mutex(); protected languageServerStartMutex = new Mutex();
protected async startLanguageServer( protected async startLanguageServer(
fqbn: string, fqbn: string,
name: string | undefined name: string | undefined,
forceStart = false
): Promise<void> { ): Promise<void> {
const port = await this.daemon.tryGetPort(); const port = await this.daemon.tryGetPort();
if (!port) { if (!port) {
@ -352,12 +357,15 @@ export class ArduinoFrontendContribution
} }
return; return;
} }
if (fqbn === this.languageServerFqbn) { if (!forceStart && fqbn === this.languageServerFqbn) {
// NOOP // NOOP
return; return;
} }
this.logger.info(`Starting language server: ${fqbn}`); this.logger.info(`Starting language server: ${fqbn}`);
const log = this.arduinoPreferences.get('arduino.language.log'); const log = this.arduinoPreferences.get('arduino.language.log');
const realTimeDiagnostics = this.arduinoPreferences.get(
'arduino.language.realTimeDiagnostics'
);
let currentSketchPath: string | undefined = undefined; let currentSketchPath: string | undefined = undefined;
if (log) { if (log) {
const currentSketch = await this.sketchServiceClient.currentSketch(); const currentSketch = await this.sketchServiceClient.currentSketch();
@ -388,6 +396,7 @@ export class ArduinoFrontendContribution
clangdPath, clangdPath,
log: currentSketchPath ? currentSketchPath : log, log: currentSketchPath ? currentSketchPath : log,
cliDaemonInstance: '1', cliDaemonInstance: '1',
realTimeDiagnostics,
board: { board: {
fqbn, fqbn,
name: name ? `"${name}"` : undefined, name: name ? `"${name}"` : undefined,

View File

@ -51,6 +51,14 @@ export const ArduinoConfigSchema: PreferenceSchema = {
), ),
default: false, default: false,
}, },
'arduino.language.realTimeDiagnostics': {
type: 'boolean',
description: nls.localize(
'arduino/preferences/language.realTimeDiagnostics',
"If true, the language server provides real-time diagnostics when typing in the editor. It's false by default."
),
default: false,
},
'arduino.compile.verbose': { 'arduino.compile.verbose': {
type: 'boolean', type: 'boolean',
description: nls.localize( description: nls.localize(
@ -238,6 +246,7 @@ export const ArduinoConfigSchema: PreferenceSchema = {
export interface ArduinoConfiguration { export interface ArduinoConfiguration {
'arduino.language.log': boolean; 'arduino.language.log': boolean;
'arduino.language.realTimeDiagnostics': boolean;
'arduino.compile.verbose': boolean; 'arduino.compile.verbose': boolean;
'arduino.compile.experimental': boolean; 'arduino.compile.experimental': boolean;
'arduino.compile.revealRange': ErrorRevealStrategy; 'arduino.compile.revealRange': ErrorRevealStrategy;

View File

@ -275,7 +275,7 @@ export class CompilerErrors
} }
private async handleCompilerErrorsDidChange( private async handleCompilerErrorsDidChange(
errors: CoreError.Compiler[] errors: CoreError.ErrorLocation[]
): Promise<void> { ): Promise<void> {
this.toDisposeOnCompilerErrorDidChange.dispose(); this.toDisposeOnCompilerErrorDidChange.dispose();
const compilerErrorsPerResource = this.groupByResource( const compilerErrorsPerResource = this.groupByResource(
@ -312,8 +312,8 @@ export class CompilerErrors
} }
private async filter( private async filter(
errors: CoreError.Compiler[] errors: CoreError.ErrorLocation[]
): Promise<CoreError.Compiler[]> { ): Promise<CoreError.ErrorLocation[]> {
if (!errors.length) { if (!errors.length) {
return []; return [];
} }
@ -326,7 +326,7 @@ export class CompilerErrors
} }
private async decorateEditors( private async decorateEditors(
errors: Map<string, CoreError.Compiler[]> errors: Map<string, CoreError.ErrorLocation[]>
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> { ): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const composite = await Promise.all( const composite = await Promise.all(
[...errors.entries()].map(([uri, errors]) => [...errors.entries()].map(([uri, errors]) =>
@ -346,7 +346,7 @@ export class CompilerErrors
private async decorateEditor( private async decorateEditor(
uri: string, uri: string,
errors: CoreError.Compiler[] errors: CoreError.ErrorLocation[]
): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> { ): Promise<{ dispose: Disposable; errors: ErrorDecoration[] }> {
const editor = await this.editorManager.getByUri(new URI(uri)); const editor = await this.editorManager.getByUri(new URI(uri));
if (!editor) { if (!editor) {
@ -523,7 +523,7 @@ export class CompilerErrors
} }
private async trackEditors( private async trackEditors(
errors: Map<string, CoreError.Compiler[]>, errors: Map<string, CoreError.ErrorLocation[]>,
...track: ((editor: EditorWidget) => Disposable)[] ...track: ((editor: EditorWidget) => Disposable)[]
): Promise<Disposable> { ): Promise<Disposable> {
return new DisposableCollection( return new DisposableCollection(
@ -605,8 +605,8 @@ export class CompilerErrors
} }
private groupByResource( private groupByResource(
errors: CoreError.Compiler[] errors: CoreError.ErrorLocation[]
): Map<string, CoreError.Compiler[]> { ): Map<string, CoreError.ErrorLocation[]> {
return errors.reduce((acc, curr) => { return errors.reduce((acc, curr) => {
const { const {
location: { uri }, location: { uri },
@ -618,7 +618,7 @@ export class CompilerErrors
} }
errors.push(curr); errors.push(curr);
return acc; return acc;
}, new Map<string, CoreError.Compiler[]>()); }, new Map<string, CoreError.ErrorLocation[]>());
} }
private monacoEditor(widget: EditorWidget): MonacoEditor | undefined; private monacoEditor(widget: EditorWidget): MonacoEditor | undefined;

View File

@ -4,29 +4,29 @@ import { CoreError } from '../../common/protocol/core-service';
@injectable() @injectable()
export class CoreErrorHandler { export class CoreErrorHandler {
private readonly compilerErrors: CoreError.Compiler[] = []; private readonly errors: CoreError.ErrorLocation[] = [];
private readonly compilerErrorsDidChangeEmitter = new Emitter< private readonly compilerErrorsDidChangeEmitter = new Emitter<
CoreError.Compiler[] CoreError.ErrorLocation[]
>(); >();
tryHandle(error: unknown): void { tryHandle(error: unknown): void {
if (CoreError.is(error)) { if (CoreError.is(error)) {
this.compilerErrors.length = 0; this.errors.length = 0;
this.compilerErrors.push(...error.data.filter(CoreError.Compiler.is)); this.errors.push(...error.data);
this.fireCompilerErrorsDidChange(); this.fireCompilerErrorsDidChange();
} }
} }
reset(): void { reset(): void {
this.compilerErrors.length = 0; this.errors.length = 0;
this.fireCompilerErrorsDidChange(); this.fireCompilerErrorsDidChange();
} }
get onCompilerErrorsDidChange(): Event<CoreError.Compiler[]> { get onCompilerErrorsDidChange(): Event<CoreError.ErrorLocation[]> {
return this.compilerErrorsDidChangeEmitter.event; return this.compilerErrorsDidChangeEmitter.event;
} }
private fireCompilerErrorsDidChange(): void { private fireCompilerErrorsDidChange(): void {
this.compilerErrorsDidChangeEmitter.fire(this.compilerErrors.slice()); this.compilerErrorsDidChangeEmitter.fire(this.errors.slice());
} }
} }

View File

@ -5,7 +5,6 @@ import type {
BoardUserField, BoardUserField,
Port, Port,
} from '../../common/protocol/boards-service'; } from '../../common/protocol/boards-service';
import type { ErrorInfo as CliErrorInfo } from '../../node/cli-error-parser';
import type { Programmer } from './boards-service'; import type { Programmer } from './boards-service';
import type { Sketch } from './sketches-service'; import type { Sketch } from './sketches-service';
@ -17,16 +16,10 @@ export const CompilerWarningLiterals = [
] as const; ] as const;
export type CompilerWarnings = typeof CompilerWarningLiterals[number]; export type CompilerWarnings = typeof CompilerWarningLiterals[number];
export namespace CoreError { export namespace CoreError {
export type ErrorInfo = CliErrorInfo; export interface ErrorLocation {
export interface Compiler extends ErrorInfo {
readonly message: string; readonly message: string;
readonly location: Location; readonly location: Location;
} readonly details?: string;
export namespace Compiler {
export function is(error: ErrorInfo): error is Compiler {
const { message, location } = error;
return !!message && !!location;
}
} }
export const Codes = { export const Codes = {
Verify: 4001, Verify: 4001,
@ -42,7 +35,7 @@ export namespace CoreError {
export const BurnBootloaderFailed = create(Codes.BurnBootloader); export const BurnBootloaderFailed = create(Codes.BurnBootloader);
export function is( export function is(
error: unknown error: unknown
): error is ApplicationError<number, ErrorInfo[]> { ): error is ApplicationError<number, ErrorLocation[]> {
return ( return (
error instanceof Error && error instanceof Error &&
ApplicationError.is(error) && ApplicationError.is(error) &&
@ -51,10 +44,10 @@ export namespace CoreError {
} }
function create( function create(
code: number code: number
): ApplicationError.Constructor<number, ErrorInfo[]> { ): ApplicationError.Constructor<number, ErrorLocation[]> {
return ApplicationError.declare( return ApplicationError.declare(
code, code,
(message: string, data: ErrorInfo[]) => { (message: string, data: ErrorLocation[]) => {
return { return {
data, data,
message, message,

View File

@ -1,24 +1,19 @@
import { notEmpty } from '@theia/core'; import { notEmpty } from '@theia/core/lib/common/objects';
import { nls } from '@theia/core/lib/common/nls'; import { nls } from '@theia/core/lib/common/nls';
import { FileUri } from '@theia/core/lib/node/file-uri'; import { FileUri } from '@theia/core/lib/node/file-uri';
import { import {
Location,
Range, Range,
Position, Position,
} from '@theia/core/shared/vscode-languageserver-protocol'; } from '@theia/core/shared/vscode-languageserver-protocol';
import { Sketch } from '../common/protocol'; import type { CoreError } from '../common/protocol';
import { Sketch } from '../common/protocol/sketches-service';
export interface ErrorInfo {
readonly message?: string;
readonly location?: Location;
readonly details?: string;
}
export interface ErrorSource { export interface ErrorSource {
readonly content: string | ReadonlyArray<Uint8Array>; readonly content: string | ReadonlyArray<Uint8Array>;
readonly sketch?: Sketch; readonly sketch?: Sketch;
} }
export function tryParseError(source: ErrorSource): ErrorInfo[] { export function tryParseError(source: ErrorSource): CoreError.ErrorLocation[] {
const { content, sketch } = source; const { content, sketch } = source;
const err = const err =
typeof content === 'string' typeof content === 'string'
@ -28,7 +23,7 @@ export function tryParseError(source: ErrorSource): ErrorInfo[] {
return tryParse(err) return tryParse(err)
.map(remapErrorMessages) .map(remapErrorMessages)
.filter(isLocationInSketch(sketch)) .filter(isLocationInSketch(sketch))
.map(errorInfo()); .map(toErrorInfo);
} }
return []; return [];
} }
@ -50,9 +45,7 @@ namespace ParseResult {
} }
} }
function isLocationInSketch( function isLocationInSketch(sketch: Sketch): (result: ParseResult) => boolean {
sketch: Sketch
): (value: ParseResult, index: number, array: ParseResult[]) => unknown {
return (result) => { return (result) => {
const uri = FileUri.create(result.path).toString(); const uri = FileUri.create(result.path).toString();
if (!Sketch.isInSketch(uri, sketch)) { if (!Sketch.isInSketch(uri, sketch)) {
@ -65,15 +58,21 @@ function isLocationInSketch(
}; };
} }
function errorInfo(): (value: ParseResult) => ErrorInfo { function toErrorInfo({
return ({ error, message, path, line, column }) => ({ error,
message,
path,
line,
column,
}: ParseResult): CoreError.ErrorLocation {
return {
message: error, message: error,
details: message, details: message,
location: { location: {
uri: FileUri.create(path).toString(), uri: FileUri.create(path).toString(),
range: range(line, column), range: range(line, column),
}, },
}); };
} }
function range(line: number, column?: number): Range { function range(line: number, column?: number): Range {

View File

@ -26,7 +26,7 @@ import { ResponseService } from '../common/protocol/response-service';
import { Board, OutputMessage, Port, Status } 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 { ArduinoCoreServiceClient } from './cli-protocol/cc/arduino/cli/commands/v1/commands_grpc_pb';
import { Port as GrpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb'; import { Port as GrpcPort } from './cli-protocol/cc/arduino/cli/commands/v1/port_pb';
import { ApplicationError, Disposable, nls } from '@theia/core'; import { ApplicationError, CommandService, Disposable, nls } from '@theia/core';
import { MonitorManager } from './monitor-manager'; import { MonitorManager } from './monitor-manager';
import { AutoFlushingBuffer } from './utils/buffers'; import { AutoFlushingBuffer } from './utils/buffers';
import { tryParseError } from './cli-error-parser'; import { tryParseError } from './cli-error-parser';
@ -42,6 +42,9 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
@inject(MonitorManager) @inject(MonitorManager)
private readonly monitorManager: MonitorManager; private readonly monitorManager: MonitorManager;
@inject(CommandService)
private readonly commandService: CommandService;
async compile( async compile(
options: CoreService.Compile.Options & { options: CoreService.Compile.Options & {
exportBinaries?: boolean; exportBinaries?: boolean;
@ -50,7 +53,19 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
): Promise<void> { ): Promise<void> {
const coreClient = await this.coreClient; const coreClient = await this.coreClient;
const { client, instance } = coreClient; const { client, instance } = coreClient;
const handler = this.createOnDataHandler(); let buildPath: string | undefined = undefined;
const handler = this.createOnDataHandler<CompileResponse>((response) => {
const currentBuildPath = response.getBuildPath();
if (!buildPath && currentBuildPath) {
buildPath = currentBuildPath;
} else {
if (!!currentBuildPath && currentBuildPath !== buildPath) {
throw new Error(
`The CLI has already provided a build path: <${buildPath}>, and there is a new build path value: <${currentBuildPath}>.`
);
}
}
});
const request = this.compileRequest(options, instance); const request = this.compileRequest(options, instance);
return new Promise<void>((resolve, reject) => { return new Promise<void>((resolve, reject) => {
client client
@ -84,7 +99,36 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
} }
}) })
.on('end', resolve); .on('end', resolve);
}).finally(() => handler.dispose()); }).finally(() => {
handler.dispose();
if (!buildPath) {
console.error(
`Have not received the build path from the CLI while running the compilation.`
);
} else {
this.fireBuildDidComplete(FileUri.create(buildPath).toString());
}
});
}
// This executes on the frontend, the VS Code extension receives it, and sends an `ino/buildDidComplete` notification to the language server.
private fireBuildDidComplete(buildOutputUri: string): void {
const params = {
buildOutputUri,
};
console.info(
`Executing 'arduino.languageserver.notifyBuildDidComplete' with ${JSON.stringify(
params
)}`
);
this.commandService
.executeCommand('arduino.languageserver.notifyBuildDidComplete', params)
.catch((err) =>
console.error(
`Unexpected error when firing event on build did complete. ${buildOutputUri}`,
err
)
);
} }
private compileRequest( private compileRequest(
@ -124,8 +168,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
options, options,
() => new UploadRequest(), () => new UploadRequest(),
(client, req) => client.upload(req), (client, req) => client.upload(req),
(message: string, info: CoreError.ErrorInfo[]) => (message: string, locations: CoreError.ErrorLocation[]) =>
CoreError.UploadFailed(message, info), CoreError.UploadFailed(message, locations),
'upload' 'upload'
); );
} }
@ -137,8 +181,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
options, options,
() => new UploadUsingProgrammerRequest(), () => new UploadUsingProgrammerRequest(),
(client, req) => client.uploadUsingProgrammer(req), (client, req) => client.uploadUsingProgrammer(req),
(message: string, info: CoreError.ErrorInfo[]) => (message: string, locations: CoreError.ErrorLocation[]) =>
CoreError.UploadUsingProgrammerFailed(message, info), CoreError.UploadUsingProgrammerFailed(message, locations),
'upload using programmer' 'upload using programmer'
); );
} }
@ -152,8 +196,8 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>, ) => ClientReadableStream<UploadResponse | UploadUsingProgrammerResponse>,
errorHandler: ( errorHandler: (
message: string, message: string,
info: CoreError.ErrorInfo[] locations: CoreError.ErrorLocation[]
) => ApplicationError<number, CoreError.ErrorInfo[]>, ) => ApplicationError<number, CoreError.ErrorLocation[]>,
task: string task: string
): Promise<void> { ): Promise<void> {
await this.compile(Object.assign(options, { exportBinaries: false })); await this.compile(Object.assign(options, { exportBinaries: false }));
@ -285,7 +329,9 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
return request; return request;
} }
private createOnDataHandler<R extends StreamingResponse>(): Disposable & { private createOnDataHandler<R extends StreamingResponse>(
onResponse?: (response: R) => void
): Disposable & {
stderr: Buffer[]; stderr: Buffer[];
onData: (response: R) => void; onData: (response: R) => void;
} { } {
@ -297,10 +343,14 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService {
} }
}); });
}); });
const onData = StreamingResponse.createOnDataHandler(stderr, (out, err) => { const onData = StreamingResponse.createOnDataHandler(
stderr,
(out, err) => {
buffer.addChunk(out); buffer.addChunk(out);
buffer.addChunk(err, OutputMessage.Severity.Error); buffer.addChunk(err, OutputMessage.Severity.Error);
}); },
onResponse
);
return { return {
dispose: () => buffer.dispose(), dispose: () => buffer.dispose(),
stderr, stderr,
@ -369,13 +419,17 @@ namespace StreamingResponse {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export function createOnDataHandler<R extends StreamingResponse>( export function createOnDataHandler<R extends StreamingResponse>(
stderr: Uint8Array[], stderr: Uint8Array[],
onData: (out: Uint8Array, err: Uint8Array) => void onData: (out: Uint8Array, err: Uint8Array) => void,
onResponse?: (response: R) => void
): (response: R) => void { ): (response: R) => void {
return (response: R) => { return (response: R) => {
const out = response.getOutStream_asU8(); const out = response.getOutStream_asU8();
const err = response.getErrStream_asU8(); const err = response.getErrStream_asU8();
stderr.push(err); stderr.push(err);
onData(out, err); onData(out, err);
if (onResponse) {
onResponse(response);
}
}; };
} }
} }

View File

@ -141,7 +141,7 @@
"theiaPluginsDir": "plugins", "theiaPluginsDir": "plugins",
"theiaPlugins": { "theiaPlugins": {
"vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.52.1/file/vscode.cpp-1.52.1.vsix", "vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.52.1/file/vscode.cpp-1.52.1.vsix",
"vscode-arduino-tools": "https://downloads.arduino.cc/vscode-arduino-tools/vscode-arduino-tools-0.0.2-beta.2.vsix", "vscode-arduino-tools": "https://downloads.arduino.cc/vscode-arduino-tools/vscode-arduino-tools-0.0.2-beta.4.vsix",
"vscode-builtin-json": "https://open-vsx.org/api/vscode/json/1.46.1/file/vscode.json-1.46.1.vsix", "vscode-builtin-json": "https://open-vsx.org/api/vscode/json/1.46.1/file/vscode.json-1.46.1.vsix",
"vscode-builtin-json-language-features": "https://open-vsx.org/api/vscode/json-language-features/1.46.1/file/vscode.json-language-features-1.46.1.vsix", "vscode-builtin-json-language-features": "https://open-vsx.org/api/vscode/json-language-features/1.46.1/file/vscode.json-language-features-1.46.1.vsix",
"cortex-debug": "https://open-vsx.org/api/marus25/cortex-debug/0.3.10/file/marus25.cortex-debug-0.3.10.vsix", "cortex-debug": "https://open-vsx.org/api/marus25/cortex-debug/0.3.10/file/marus25.cortex-debug-0.3.10.vsix",

View File

@ -271,6 +271,7 @@
"invalid.sketchbook.location": "Invalid sketchbook location: {0}", "invalid.sketchbook.location": "Invalid sketchbook location: {0}",
"invalid.theme": "Invalid theme.", "invalid.theme": "Invalid theme.",
"language.log": "True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.", "language.log": "True if the Arduino Language Server should generate log files into the sketch folder. Otherwise, false. It's false by default.",
"language.realTimeDiagnostics": "If true, the language server provides real-time diagnostics when typing in the editor. It's false by default.",
"manualProxy": "Manual proxy configuration", "manualProxy": "Manual proxy configuration",
"network": "Network", "network": "Network",
"newSketchbookLocation": "Select new sketchbook location", "newSketchbookLocation": "Select new sketchbook location",

View File

@ -75,7 +75,7 @@
"theiaPluginsDir": "plugins", "theiaPluginsDir": "plugins",
"theiaPlugins": { "theiaPlugins": {
"vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.52.1/file/vscode.cpp-1.52.1.vsix", "vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.52.1/file/vscode.cpp-1.52.1.vsix",
"vscode-arduino-tools": "https://downloads.arduino.cc/vscode-arduino-tools/vscode-arduino-tools-0.0.2-beta.2.vsix", "vscode-arduino-tools": "https://downloads.arduino.cc/vscode-arduino-tools/vscode-arduino-tools-0.0.2-beta.4.vsix",
"vscode-builtin-json": "https://open-vsx.org/api/vscode/json/1.46.1/file/vscode.json-1.46.1.vsix", "vscode-builtin-json": "https://open-vsx.org/api/vscode/json/1.46.1/file/vscode.json-1.46.1.vsix",
"vscode-builtin-json-language-features": "https://open-vsx.org/api/vscode/json-language-features/1.46.1/file/vscode.json-language-features-1.46.1.vsix", "vscode-builtin-json-language-features": "https://open-vsx.org/api/vscode/json-language-features/1.46.1/file/vscode.json-language-features-1.46.1.vsix",
"cortex-debug": "https://open-vsx.org/api/marus25/cortex-debug/0.3.10/file/marus25.cortex-debug-0.3.10.vsix", "cortex-debug": "https://open-vsx.org/api/marus25/cortex-debug/0.3.10/file/marus25.cortex-debug-0.3.10.vsix",