diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index bb1963ee6..f654837df 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -104,7 +104,8 @@ "temp": "^0.9.1", "temp-dir": "^2.0.0", "tree-kill": "^1.2.1", - "util": "^0.12.5" + "util": "^0.12.5", + "vscode-arduino-api": "^0.1.2" }, "devDependencies": { "@octokit/rest": "^18.12.0", diff --git a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts index 89d13fd93..ea568d595 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -354,6 +354,11 @@ import { FileResourceResolver as TheiaFileResourceResolver } from '@theia/filesy import { StylingParticipant } from '@theia/core/lib/browser/styling-service'; import { MonacoEditorMenuContribution } from './theia/monaco/monaco-menu'; import { MonacoEditorMenuContribution as TheiaMonacoEditorMenuContribution } from '@theia/monaco/lib/browser/monaco-menu'; +import { UpdateArduinoState } from './contributions/update-arduino-state'; +import { TerminalWidgetImpl } from './theia/terminal/terminal-widget-impl'; +import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; +import { TerminalFrontendContribution } from './theia/terminal/terminal-frontend-contribution'; +import { TerminalFrontendContribution as TheiaTerminalFrontendContribution } from '@theia/terminal/lib/browser/terminal-frontend-contribution' // Hack to fix copy/cut/paste issue after electron version update in Theia. // https://github.com/eclipse-theia/theia/issues/12487 @@ -747,6 +752,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { Contribution.configure(bind, Account); Contribution.configure(bind, CloudSketchbookContribution); Contribution.configure(bind, CreateCloudCopy); + Contribution.configure(bind, UpdateArduinoState); bindContributionProvider(bind, StartupTaskProvider); bind(StartupTaskProvider).toService(BoardsServiceProvider); // to reuse the boards config in another window @@ -1024,4 +1030,9 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TheiaMonacoEditorMenuContribution).toService( MonacoEditorMenuContribution ); + + // Patch terminal issues. + rebind(TerminalWidget).to(TerminalWidgetImpl).inTransientScope(); + bind(TerminalFrontendContribution).toSelf().inSingletonScope(); + rebind(TheiaTerminalFrontendContribution).toService(TerminalFrontendContribution); }); diff --git a/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts new file mode 100644 index 000000000..a227a51a0 --- /dev/null +++ b/arduino-ide-extension/src/browser/contributions/update-arduino-state.ts @@ -0,0 +1,179 @@ +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import URI from '@theia/core/lib/common/uri'; +import { inject, injectable } from '@theia/core/shared/inversify'; +import { HostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import type { ArduinoState } from 'vscode-arduino-api'; +import { + BoardsService, + CompileSummary, + Port, + isCompileSummary, +} from '../../common/protocol'; +import { + toApiBoardDetails, + toApiCompileSummary, + toApiPort, +} from '../../common/protocol/arduino-context-mapper'; +import type { BoardsConfig } from '../boards/boards-config'; +import { BoardsDataStore } from '../boards/boards-data-store'; +import { BoardsServiceProvider } from '../boards/boards-service-provider'; +import { CurrentSketch } from '../sketches-service-client-impl'; +import { SketchContribution } from './contribution'; + +interface UpdateStateParams { + readonly key: keyof T; + readonly value: T[keyof T]; +} + +/** + * Contribution for updating the Arduino state, such as the FQBN, selected port, and sketch path changes via commands, so other VS Code extensions can access it. + * See [`vscode-arduino-api`](https://github.com/dankeboy36/vscode-arduino-api#api) for more details. + */ +@injectable() +export class UpdateArduinoState extends SketchContribution { + @inject(BoardsService) + private readonly boardsService: BoardsService; + @inject(BoardsServiceProvider) + private readonly boardsServiceProvider: BoardsServiceProvider; + @inject(BoardsDataStore) + private readonly boardsDataStore: BoardsDataStore; + @inject(HostedPluginSupport) + private readonly hostedPluginSupport: HostedPluginSupport; + + private readonly toDispose = new DisposableCollection(); + + override onStart(): void { + this.toDispose.pushAll([ + this.boardsServiceProvider.onBoardsConfigChanged((config) => + this.updateBoardsConfig(config) + ), + this.sketchServiceClient.onCurrentSketchDidChange((sketch) => + this.updateSketchPath(sketch) + ), + this.configService.onDidChangeDataDirUri((dataDirUri) => + this.updateDataDirPath(dataDirUri) + ), + this.configService.onDidChangeSketchDirUri((userDirUri) => + this.updateUserDirPath(userDirUri) + ), + this.commandService.onDidExecuteCommand(({ commandId, args }) => { + if ( + commandId === 'arduino.languageserver.notifyBuildDidComplete' && + isCompileSummary(args[0]) + ) { + this.updateCompileSummary(args[0]); + } + }), + this.boardsDataStore.onChanged((fqbn) => { + const selectedFqbn = + this.boardsServiceProvider.boardsConfig.selectedBoard?.fqbn; + if (selectedFqbn && fqbn.includes(selectedFqbn)) { + this.updateBoardDetails(selectedFqbn); + } + }), + ]); + } + + override onReady(): void { + this.boardsServiceProvider.reconciled.then(() => { + this.updateBoardsConfig(this.boardsServiceProvider.boardsConfig); + }); + this.updateSketchPath(this.sketchServiceClient.tryGetCurrentSketch()); + this.updateUserDirPath(this.configService.tryGetSketchDirUri()); + this.updateDataDirPath(this.configService.tryGetDataDirUri()); + } + + onStop(): void { + this.toDispose.dispose(); + } + + private async updateSketchPath( + sketch: CurrentSketch | undefined + ): Promise { + const sketchPath = CurrentSketch.isValid(sketch) + ? new URI(sketch.uri).path.fsPath() + : undefined; + return this.updateState({ key: 'sketchPath', value: sketchPath }); + } + + private async updateCompileSummary( + compileSummary: CompileSummary + ): Promise { + const apiCompileSummary = toApiCompileSummary(compileSummary); + return this.updateState({ + key: 'compileSummary', + value: apiCompileSummary, + }); + } + + private async updateBoardsConfig( + boardsConfig: BoardsConfig.Config + ): Promise { + const fqbn = boardsConfig.selectedBoard?.fqbn; + const port = boardsConfig.selectedPort; + await this.updateFqbn(fqbn); + await this.updateBoardDetails(fqbn); + await this.updatePort(port); + } + + private async updateFqbn(fqbn: string | undefined): Promise { + await this.updateState({ key: 'fqbn', value: fqbn }); + } + + private async updateBoardDetails(fqbn: string | undefined): Promise { + const unset = () => + this.updateState({ key: 'boardDetails', value: undefined }); + if (!fqbn) { + return unset(); + } + const [details, persistedData] = await Promise.all([ + this.boardsService.getBoardDetails({ fqbn }), + this.boardsDataStore.getData(fqbn), + ]); + if (!details) { + return unset(); + } + const apiBoardDetails = toApiBoardDetails({ + ...details, + configOptions: + BoardsDataStore.Data.EMPTY === persistedData + ? details.configOptions + : persistedData.configOptions.slice(), + }); + return this.updateState({ + key: 'boardDetails', + value: apiBoardDetails, + }); + } + + private async updatePort(port: Port | undefined): Promise { + const apiPort = port && toApiPort(port); + return this.updateState({ key: 'port', value: apiPort }); + } + + private async updateUserDirPath(userDirUri: URI | undefined): Promise { + const userDirPath = userDirUri?.path.fsPath(); + return this.updateState({ + key: 'userDirPath', + value: userDirPath, + }); + } + + private async updateDataDirPath(dataDirUri: URI | undefined): Promise { + const dataDirPath = dataDirUri?.path.fsPath(); + return this.updateState({ + key: 'dataDirPath', + value: dataDirPath, + }); + } + + private async updateState( + params: UpdateStateParams + ): Promise { + await this.hostedPluginSupport.didStart; + return this.commandService.executeCommand( + 'arduinoAPI.updateState', + params + ); + } +} diff --git a/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts b/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts index 326e02ee4..8491357a4 100644 --- a/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts +++ b/arduino-ide-extension/src/browser/theia/plugin-ext/hosted-plugin.ts @@ -1,7 +1,10 @@ -import { Emitter, Event, JsonRpcProxy } from '@theia/core'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { Emitter, Event } from '@theia/core/lib/common/event'; import { injectable, interfaces } from '@theia/core/shared/inversify'; -import { HostedPluginServer } from '@theia/plugin-ext/lib/common/plugin-protocol'; -import { HostedPluginSupport as TheiaHostedPluginSupport } from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; +import { + PluginContributions, + HostedPluginSupport as TheiaHostedPluginSupport, +} from '@theia/plugin-ext/lib/hosted/browser/hosted-plugin'; @injectable() export class HostedPluginSupport extends TheiaHostedPluginSupport { @@ -10,7 +13,7 @@ export class HostedPluginSupport extends TheiaHostedPluginSupport { override onStart(container: interfaces.Container): void { super.onStart(container); - this.hostedPluginServer.onDidCloseConnection(() => + this['server'].onDidCloseConnection(() => this.onDidCloseConnectionEmitter.fire() ); } @@ -28,8 +31,38 @@ export class HostedPluginSupport extends TheiaHostedPluginSupport { return this.onDidCloseConnectionEmitter.event; } - private get hostedPluginServer(): JsonRpcProxy { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - return (this as any).server; + protected override startPlugins( + contributionsByHost: Map, + toDisconnect: DisposableCollection + ): Promise { + reorderPlugins(contributionsByHost); + return super.startPlugins(contributionsByHost, toDisconnect); } } + +/** + * Force the `vscode-arduino-ide` API to activate before any Arduino IDE tool VSIX. + * + * Arduino IDE tool VISXs are not forced to declare the `vscode-arduino-api` as a `extensionDependencies`, + * but the API must activate before any tools. This in place sorting helps to bypass Theia's plugin resolution + * without forcing tools developers to add `vscode-arduino-api` to the `extensionDependencies`. + */ +function reorderPlugins( + contributionsByHost: Map +): void { + for (const [, contributions] of contributionsByHost) { + const apiPluginIndex = contributions.findIndex(isArduinoAPI); + if (apiPluginIndex >= 0) { + const apiPlugin = contributions[apiPluginIndex]; + contributions.splice(apiPluginIndex, 1); + contributions.unshift(apiPlugin); + } + } +} + +function isArduinoAPI(pluginContribution: PluginContributions): boolean { + return ( + pluginContribution.plugin.metadata.model.id === + 'dankeboy36.vscode-arduino-api' + ); +} diff --git a/arduino-ide-extension/src/browser/theia/terminal/terminal-frontend-contribution.ts b/arduino-ide-extension/src/browser/theia/terminal/terminal-frontend-contribution.ts new file mode 100644 index 000000000..13020175d --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/terminal/terminal-frontend-contribution.ts @@ -0,0 +1,38 @@ +import { TabBarToolbarRegistry } from '@theia/core/lib/browser/shell/tab-bar-toolbar'; +import { CommandRegistry } from '@theia/core/lib/common/command'; +import { Widget } from '@theia/core/shared/@phosphor/widgets'; +import { injectable } from '@theia/core/shared/inversify'; +import { TerminalWidget } from '@theia/terminal/lib/browser/base/terminal-widget'; +import { + TerminalCommands, + TerminalFrontendContribution as TheiaTerminalFrontendContribution, +} from '@theia/terminal/lib/browser/terminal-frontend-contribution'; + +// Patch for https://github.com/eclipse-theia/theia/pull/12626 +@injectable() +export class TerminalFrontendContribution extends TheiaTerminalFrontendContribution { + override registerCommands(commands: CommandRegistry): void { + super.registerCommands(commands); + commands.unregisterCommand(TerminalCommands.SPLIT); + commands.registerCommand(TerminalCommands.SPLIT, { + execute: () => this.splitTerminal(), + isEnabled: (w) => this.withWidget(w, () => true), + isVisible: (w) => this.withWidget(w, () => true), + }); + } + + override registerToolbarItems(toolbar: TabBarToolbarRegistry): void { + super.registerToolbarItems(toolbar); + toolbar.unregisterItem(TerminalCommands.SPLIT.id); + } + + private withWidget( + widget: Widget | undefined, + fn: (widget: TerminalWidget) => T + ): T | false { + if (widget instanceof TerminalWidget) { + return fn(widget); + } + return false; + } +} diff --git a/arduino-ide-extension/src/browser/theia/terminal/terminal-widget-impl.ts b/arduino-ide-extension/src/browser/theia/terminal/terminal-widget-impl.ts new file mode 100644 index 000000000..310a659d1 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/terminal/terminal-widget-impl.ts @@ -0,0 +1,23 @@ +import { injectable } from '@theia/core/shared/inversify'; +import { TerminalWidgetImpl as TheiaTerminalWidgetImpl } from '@theia/terminal/lib/browser/terminal-widget-impl'; +import debounce from 'p-debounce'; + +// Patch for https://github.com/eclipse-theia/theia/pull/12587 +@injectable() +export class TerminalWidgetImpl extends TheiaTerminalWidgetImpl { + private readonly debouncedResizeTerminal = debounce( + () => this.doResizeTerminal(), + 50 + ); + + protected override resizeTerminal(): void { + this.debouncedResizeTerminal(); + } + + private doResizeTerminal(): void { + const geo = this.fitAddon.proposeDimensions(); + const cols = geo.cols; + const rows = geo.rows - 1; // subtract one row for margin + this.term.resize(cols, rows); + } +} diff --git a/arduino-ide-extension/src/common/protocol/arduino-context-mapper.ts b/arduino-ide-extension/src/common/protocol/arduino-context-mapper.ts new file mode 100644 index 000000000..ede24fe1c --- /dev/null +++ b/arduino-ide-extension/src/common/protocol/arduino-context-mapper.ts @@ -0,0 +1,126 @@ +import type { + Port as APIPort, + BoardDetails as ApiBoardDetails, + BuildProperties as ApiBuildProperties, + CompileSummary as ApiCompileSummary, + ConfigOption as ApiConfigOption, + ConfigValue as ApiConfigValue, + Tool as ApiTool, +} from 'vscode-arduino-api'; +import type { + BoardDetails, + CompileSummary, + ConfigOption, + ConfigValue, + Port, + Tool, +} from '../protocol'; + +export function toApiCompileSummary( + compileSummary: CompileSummary +): ApiCompileSummary { + const { + buildPath, + buildProperties, + boardPlatform, + buildPlatform, + executableSectionsSize, + usedLibraries, + } = compileSummary; + return { + buildPath, + buildProperties: toApiBuildProperties(buildProperties), + executableSectionsSize: executableSectionsSize, + boardPlatform, + buildPlatform, + usedLibraries, + }; +} + +export function toApiPort(port: Port): APIPort | undefined { + const { + hardwareId = '', + properties = {}, + address, + protocol, + protocolLabel, + addressLabel: label, + } = port; + return { + label, + address, + hardwareId, + properties, + protocol, + protocolLabel, + }; +} + +export function toApiBoardDetails(boardDetails: BoardDetails): ApiBoardDetails { + const { fqbn, programmers, configOptions, requiredTools } = boardDetails; + return { + buildProperties: toApiBuildProperties(boardDetails.buildProperties), + configOptions: configOptions.map(toApiConfigOption), + fqbn, + programmers, + toolsDependencies: requiredTools.map(toApiTool), + }; +} + +function toApiConfigOption(configOption: ConfigOption): ApiConfigOption { + const { label, values, option } = configOption; + return { + optionLabel: label, + option, + values: values.map(toApiConfigValue), + }; +} + +function toApiConfigValue(configValue: ConfigValue): ApiConfigValue { + const { label, selected, value } = configValue; + return { + selected, + value, + valueLabel: label, + }; +} + +function toApiTool(toolDependency: Tool): ApiTool { + const { name, packager, version } = toolDependency; + return { + name, + packager, + version, + }; +} + +const propertySep = '='; + +function parseProperty( + property: string +): [key: string, value: string] | undefined { + const segments = property.split(propertySep); + if (segments.length < 2) { + console.warn(`Could not parse build property: ${property}.`); + return undefined; + } + + const [key, ...rest] = segments; + if (!key) { + console.warn(`Could not determine property key from raw: ${property}.`); + return undefined; + } + const value = rest.join(propertySep); + return [key, value]; +} + +export function toApiBuildProperties(properties: string[]): ApiBuildProperties { + return properties.reduce((acc, curr) => { + const entry = parseProperty(curr); + if (entry) { + const [key, value] = entry; + acc[key] = value; + } + return acc; + }, >{}); +} diff --git a/arduino-ide-extension/src/common/protocol/boards-service.ts b/arduino-ide-extension/src/common/protocol/boards-service.ts index c955b9462..7e2f77555 100644 --- a/arduino-ide-extension/src/common/protocol/boards-service.ts +++ b/arduino-ide-extension/src/common/protocol/boards-service.ts @@ -441,6 +441,7 @@ export interface BoardDetails { readonly debuggingSupported: boolean; readonly VID: string; readonly PID: string; + readonly buildProperties: string[]; } export interface Tool { diff --git a/arduino-ide-extension/src/common/protocol/core-service.ts b/arduino-ide-extension/src/common/protocol/core-service.ts index 7dfcfb896..2a683370d 100644 --- a/arduino-ide-extension/src/common/protocol/core-service.ts +++ b/arduino-ide-extension/src/common/protocol/core-service.ts @@ -5,13 +5,11 @@ import type { Range, Position, } from '@theia/core/shared/vscode-languageserver-protocol'; -import type { - BoardUserField, - Port, -} from '../../common/protocol/boards-service'; +import type { BoardUserField, Port, Installable } from '../../common/protocol/'; import type { Programmer } from './boards-service'; import type { Sketch } from './sketches-service'; import { IndexUpdateSummary } from './notification-service'; +import type { CompileSummary as ApiCompileSummary } from 'vscode-arduino-api'; export const CompilerWarningLiterals = [ 'None', @@ -19,7 +17,7 @@ export const CompilerWarningLiterals = [ 'More', 'All', ] as const; -export type CompilerWarnings = typeof CompilerWarningLiterals[number]; +export type CompilerWarnings = (typeof CompilerWarningLiterals)[number]; export namespace CompilerWarnings { export function labelOf(warning: CompilerWarnings): string { return CompilerWarningLabels[warning]; @@ -103,6 +101,53 @@ export namespace CoreError { } } +export interface InstalledPlatformReference { + readonly id: string; + readonly version: Installable.Version; + /** + * Absolute filesystem path. + */ + readonly installDir: string; + readonly packageUrl: string; +} + +export interface ExecutableSectionSize { + readonly name: string; + readonly size: number; + readonly maxSize: number; +} + +export interface CompileSummary { + readonly buildPath: string; + /** + * To be compatible with the `vscode-arduino-tools` API. + * @deprecated Use `buildPath` instead. Use Theia or VS Code URI to convert to an URI string on the client side. + */ + readonly buildOutputUri: string; + readonly usedLibraries: ApiCompileSummary['usedLibraries']; + readonly executableSectionsSize: ExecutableSectionSize[]; + readonly boardPlatform?: InstalledPlatformReference | undefined; + readonly buildPlatform?: InstalledPlatformReference | undefined; + readonly buildProperties: string[]; +} + +export function isCompileSummary(arg: unknown): arg is CompileSummary { + return ( + Boolean(arg) && + typeof arg === 'object' && + (arg).buildPath !== undefined && + typeof (arg).buildPath === 'string' && + (arg).buildOutputUri !== undefined && + typeof (arg).buildOutputUri === 'string' && + (arg).executableSectionsSize !== undefined && + Array.isArray((arg).executableSectionsSize) && + (arg).usedLibraries !== undefined && + Array.isArray((arg).usedLibraries) && + (arg).buildProperties !== undefined && + Array.isArray((arg).buildProperties) + ); +} + export const CoreServicePath = '/services/core-service'; export const CoreService = Symbol('CoreService'); export interface CoreService { @@ -132,7 +177,7 @@ export interface CoreService { } export const IndexTypeLiterals = ['platform', 'library'] as const; -export type IndexType = typeof IndexTypeLiterals[number]; +export type IndexType = (typeof IndexTypeLiterals)[number]; export namespace IndexType { export function is(arg: unknown): arg is IndexType { return ( diff --git a/arduino-ide-extension/src/node/boards-service-impl.ts b/arduino-ide-extension/src/node/boards-service-impl.ts index e04aa909f..a2db13193 100644 --- a/arduino-ide-extension/src/node/boards-service-impl.ts +++ b/arduino-ide-extension/src/node/boards-service-impl.ts @@ -155,6 +155,7 @@ export class BoardsServiceImpl VID = prop.get('vid') || ''; PID = prop.get('pid') || ''; } + const buildProperties = detailsResp.getBuildPropertiesList(); return { fqbn, @@ -164,6 +165,7 @@ export class BoardsServiceImpl debuggingSupported, VID, PID, + buildProperties }; } diff --git a/arduino-ide-extension/src/node/core-service-impl.ts b/arduino-ide-extension/src/node/core-service-impl.ts index e5e3d05ac..253dcd383 100644 --- a/arduino-ide-extension/src/node/core-service-impl.ts +++ b/arduino-ide-extension/src/node/core-service-impl.ts @@ -8,6 +8,8 @@ import { CompilerWarnings, CoreService, CoreError, + CompileSummary, + isCompileSummary, } from '../common/protocol/core-service'; import { CompileRequest, @@ -35,12 +37,15 @@ import { firstToUpperCase, notEmpty } from '../common/utils'; import { ServiceError } from './service-error'; import { ExecuteWithProgress, ProgressResponse } from './grpc-progressible'; import { BoardDiscovery } from './board-discovery'; +import { Mutable } from '@theia/core/lib/common/types'; namespace Uploadable { export type Request = UploadRequest | UploadUsingProgrammerRequest; export type Response = UploadResponse | UploadUsingProgrammerResponse; } +type CompileSummaryFragment = Partial>; + @injectable() export class CoreServiceImpl extends CoreClientAware implements CoreService { @inject(ResponseService) @@ -58,23 +63,13 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { async compile(options: CoreService.Options.Compile): Promise { const coreClient = await this.coreClient; const { client, instance } = coreClient; - let buildPath: string | undefined = undefined; + const compileSummary = {}; const progressHandler = this.createProgressHandler(options); - const buildPathHandler = (response: CompileResponse) => { - const currentBuildPath = response.getBuildPath(); - if (currentBuildPath) { - buildPath = currentBuildPath; - } else { - if (!!buildPath && currentBuildPath !== buildPath) { - throw new Error( - `The CLI has already provided a build path: <${buildPath}>, and IDE received a new build path value: <${currentBuildPath}>.` - ); - } - } - }; + const compileSummaryHandler = (response: CompileResponse) => + updateCompileSummary(compileSummary, response); const handler = this.createOnDataHandler( progressHandler, - buildPathHandler + compileSummaryHandler ); const request = this.compileRequest(options, instance); return new Promise((resolve, reject) => { @@ -111,31 +106,35 @@ export class CoreServiceImpl extends CoreClientAware implements CoreService { .on('end', resolve); }).finally(() => { handler.dispose(); - if (!buildPath) { + if (!isCompileSummary(compileSummary)) { console.error( - `Have not received the build path from the CLI while running the compilation.` + `Have not received the full compile summary from the CLI while running the compilation. ${JSON.stringify( + compileSummary + )}` ); } else { - this.fireBuildDidComplete(FileUri.create(buildPath).toString()); + this.fireBuildDidComplete(compileSummary); } }); } // 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 { + private fireBuildDidComplete(compileSummary: CompileSummary): void { const params = { - buildOutputUri, + ...compileSummary, }; console.info( `Executing 'arduino.languageserver.notifyBuildDidComplete' with ${JSON.stringify( - params + params.buildOutputUri )}` ); this.commandService .executeCommand('arduino.languageserver.notifyBuildDidComplete', params) .catch((err) => console.error( - `Unexpected error when firing event on build did complete. ${buildOutputUri}`, + `Unexpected error when firing event on build did complete. ${JSON.stringify( + params.buildOutputUri + )}`, err ) ); @@ -465,3 +464,62 @@ namespace StreamingResponse { readonly handlers?: ((response: R) => void)[]; } } + +function updateCompileSummary( + compileSummary: CompileSummaryFragment, + response: CompileResponse +): CompileSummaryFragment { + const buildPath = response.getBuildPath(); + if (buildPath) { + compileSummary.buildPath = buildPath; + compileSummary.buildOutputUri = FileUri.create(buildPath).toString(); + } + const executableSectionsSize = response.getExecutableSectionsSizeList(); + if (executableSectionsSize) { + compileSummary.executableSectionsSize = executableSectionsSize.map((item) => + item.toObject(false) + ); + } + const usedLibraries = response.getUsedLibrariesList(); + if (usedLibraries) { + compileSummary.usedLibraries = usedLibraries.map((item) => { + const object = item.toObject(false); + const library = { + ...object, + architectures: object.architecturesList, + types: object.typesList, + examples: object.examplesList, + providesIncludes: object.providesIncludesList, + properties: object.propertiesMap.reduce((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {} as Record), + compatibleWith: object.compatibleWithMap.reduce((acc, [key, value]) => { + acc[key] = value; + return acc; + }, {} as Record), + } as const; + const mutable = >>library; + delete mutable.architecturesList; + delete mutable.typesList; + delete mutable.examplesList; + delete mutable.providesIncludesList; + delete mutable.propertiesMap; + delete mutable.compatibleWithMap; + return library; + }); + } + const boardPlatform = response.getBoardPlatform(); + if (boardPlatform) { + compileSummary.buildPlatform = boardPlatform.toObject(false); + } + const buildPlatform = response.getBuildPlatform(); + if (buildPlatform) { + compileSummary.buildPlatform = buildPlatform.toObject(false); + } + const buildProperties = response.getBuildPropertiesList(); + if (buildProperties) { + compileSummary.buildProperties = buildProperties.slice(); + } + return compileSummary; +} diff --git a/arduino-ide-extension/src/test/common/arduino-context-mapper.test.ts b/arduino-ide-extension/src/test/common/arduino-context-mapper.test.ts new file mode 100644 index 000000000..23727bffc --- /dev/null +++ b/arduino-ide-extension/src/test/common/arduino-context-mapper.test.ts @@ -0,0 +1,43 @@ +import { expect } from 'chai'; +import { toApiBuildProperties } from '../../common/protocol/arduino-context-mapper'; + +describe('arduino-context-mapper', () => { + describe('toApiBuildProperties', () => { + it('should parse an array of build properties string into a record', () => { + const expected = { + foo: 'alma', + bar: '36', + baz: 'false', + }; + const actual = toApiBuildProperties(['foo=alma', 'bar=36', 'baz=false']); + expect(actual).to.be.deep.equal(expected); + }); + + it('should not skip build property key with empty value', () => { + const expected = { + foo: '', + }; + const actual = toApiBuildProperties(['foo=']); + expect(actual).to.be.deep.equal(expected); + }); + + it('should skip invalid entries', () => { + const expected = { + foo: 'alma', + bar: '36', + baz: '-DARDUINO_USB_CDC_ON_BOOT=0', + }; + const actual = toApiBuildProperties([ + 'foo=alma', + 'invalid', + '=invalid2', + '=invalid3=', + '=', + '==', + 'bar=36', + 'baz=-DARDUINO_USB_CDC_ON_BOOT=0', + ]); + expect(actual).to.be.deep.equal(expected); + }); + }); +}); diff --git a/arduino-ide-extension/src/test/node/core-service-impl.slow-test.ts b/arduino-ide-extension/src/test/node/core-service-impl.slow-test.ts index faac7b6c7..0a0fd253c 100644 --- a/arduino-ide-extension/src/test/node/core-service-impl.slow-test.ts +++ b/arduino-ide-extension/src/test/node/core-service-impl.slow-test.ts @@ -9,6 +9,7 @@ import { BoardsService, CoreService, SketchesService, + isCompileSummary, } from '../../common/protocol'; import { createBaseContainer, startDaemon } from './test-bindings'; @@ -31,7 +32,7 @@ describe('core-service-impl', () => { afterEach(() => toDispose.dispose()); describe('compile', () => { - it('should execute a command with the build path', async function () { + it('should execute a command with the compile summary, including the build path', async function () { this.timeout(testTimeout); const coreService = container.get(CoreService); const sketchesService = container.get(SketchesService); @@ -56,7 +57,7 @@ describe('core-service-impl', () => { const [, args] = executedBuildDidCompleteCommands[0]; expect(args.length).to.be.equal(1); const arg = args[0]; - expect(typeof arg).to.be.equal('object'); + expect(isCompileSummary(arg)).to.be.true; expect('buildOutputUri' in arg).to.be.true; expect(arg.buildOutputUri).to.be.not.undefined; diff --git a/package.json b/package.json index 9c9c00f56..6cfd1c915 100644 --- a/package.json +++ b/package.json @@ -70,6 +70,7 @@ "theiaPluginsDir": "plugins", "theiaPlugins": { "vscode-builtin-cpp": "https://open-vsx.org/api/vscode/cpp/1.52.1/file/vscode.cpp-1.52.1.vsix", + "vscode-arduino-api": "https://github.com/dankeboy36/vscode-arduino-api/releases/download/0.1.2/vscode-arduino-api-0.1.2.vsix", "vscode-arduino-tools": "https://downloads.arduino.cc/vscode-arduino-tools/vscode-arduino-tools-0.0.2-beta.8.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", diff --git a/yarn.lock b/yarn.lock index e5a69a4c7..74640bac9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3209,6 +3209,11 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-7.0.5.tgz#b1d2f772142a301538fae9bdf9cf15b9f2573a29" integrity sha512-hKB88y3YHL8oPOs/CNlaXtjWn93+Bs48sDQR37ZUqG2tLeCS7EA1cmnkKsuQsub9OKEB/y/Rw9zqJqqNSbqVlQ== +"@types/vscode@^1.78.0": + version "1.78.0" + resolved "https://registry.yarnpkg.com/@types/vscode/-/vscode-1.78.0.tgz#b5600abce8855cf21fb32d0857bcd084b1f83069" + integrity sha512-LJZIJpPvKJ0HVQDqfOy6W4sNKUBBwyDu1Bs8chHBZOe9MNuKTJtidgZ2bqjhmmWpUb0TIIqv47BFUcVmAsgaVA== + "@types/which@^1.3.1": version "1.3.2" resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf" @@ -3991,6 +3996,14 @@ arduino-serial-plotter-webapp@0.2.0: resolved "https://registry.yarnpkg.com/arduino-serial-plotter-webapp/-/arduino-serial-plotter-webapp-0.2.0.tgz#90d61ad7ed1452f70fd226ff25eccb36c1ab1a4f" integrity sha512-AxQIsKr6Mf8K1c3kj+ojjFvE9Vz8cUqJqRink6/myp/ranEGwsQQ83hziktkPKZvBQshqrMH8nzoGIY2Z3A2OA== +ardunno-cli@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/ardunno-cli/-/ardunno-cli-0.1.2.tgz#145d998231b34b33bf70f7fc6e5be6497191f708" + integrity sha512-8PTBMDS2ofe2LJZZKHw/MgfXgDwpiImXJcBeqeZ6lcTSDqQNMJpEIjcCdPcxbsQbJXRRfZZ4nn6G/gXwEuJPpw== + dependencies: + nice-grpc-common "^2.0.2" + protobufjs "^7.2.3" + are-we-there-yet@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz#372e0e7bd279d8e94c653aaa1f67200884bf3e1c" @@ -10738,6 +10751,13 @@ nested-error-stacks@^2.1.1: resolved "https://registry.yarnpkg.com/nested-error-stacks/-/nested-error-stacks-2.1.1.tgz#26c8a3cee6cc05fbcf1e333cd2fc3e003326c0b5" integrity sha512-9iN1ka/9zmX1ZvLV9ewJYEk9h7RyRRtqdK0woXcqohu8EWIerfPUjYJPg0ULy0UqP7cslmdGc8xKDJcojlKiaw== +nice-grpc-common@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/nice-grpc-common/-/nice-grpc-common-2.0.2.tgz#e6aeebb2bd19d87114b351e291e30d79dd38acf7" + integrity sha512-7RNWbls5kAL1QVUOXvBsv1uO0wPQK3lHv+cY1gwkTzirnG1Nop4cBJZubpgziNbaVc/bl9QJcyvsf/NQxa3rjQ== + dependencies: + ts-error "^1.0.6" + nice-try@^1.0.4: version "1.0.5" resolved "https://registry.yarnpkg.com/nice-try/-/nice-try-1.0.5.tgz#a3378a7696ce7d223e88fc9b764bd7ef1089e366" @@ -12184,7 +12204,7 @@ proto-list@~1.2.1: resolved "https://registry.yarnpkg.com/proto-list/-/proto-list-1.2.4.tgz#212d5bfe1318306a420f6402b8e26ff39647a849" integrity sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA== -protobufjs@^7.0.0: +protobufjs@^7.0.0, protobufjs@^7.2.3: version "7.2.3" resolved "https://registry.yarnpkg.com/protobufjs/-/protobufjs-7.2.3.tgz#01af019e40d9c6133c49acbb3ff9e30f4f0f70b2" integrity sha512-TtpvOqwB5Gdz/PQmOjgsrGH1nHjAQVCN7JG4A6r1sXRWESL5rNMAiRcBQlCAdKxZcAbstExQePYG8xof/JVRgg== @@ -13083,6 +13103,11 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" +safe-stable-stringify@^2.4.3: + version "2.4.3" + resolved "https://registry.yarnpkg.com/safe-stable-stringify/-/safe-stable-stringify-2.4.3.tgz#138c84b6f6edb3db5f8ef3ef7115b8f55ccbf886" + integrity sha512-e2bDA2WJT0wxseVd4lsDP4+3ONX6HpMXQa1ZhFQ7SU+GjvORCmShbCMltrtIDfkYhVHrOcPtj+KhmDBdPdZD1g== + "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" @@ -14350,6 +14375,11 @@ trough@^2.0.0: resolved "https://registry.yarnpkg.com/trough/-/trough-2.1.0.tgz#0f7b511a4fde65a46f18477ab38849b22c554876" integrity sha512-AqTiAOLcj85xS7vQ8QkAV41hPDIJ71XJB4RCUrzo/1GM2CQwhkJGaf9Hgr7BOugMRpgGUrqRg/DrBDl4H40+8g== +ts-error@^1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/ts-error/-/ts-error-1.0.6.tgz#277496f2a28de6c184cfce8dfd5cdd03a4e6b0fc" + integrity sha512-tLJxacIQUM82IR7JO1UUkKlYuUTmoY9HBJAmNWFzheSlDS5SPMcNIepejHJa4BpPQLAcbRhRf3GDJzyj6rbKvA== + ts-md5@^1.2.2: version "1.3.1" resolved "https://registry.yarnpkg.com/ts-md5/-/ts-md5-1.3.1.tgz#f5b860c0d5241dd9bb4e909dd73991166403f511" @@ -14947,6 +14977,15 @@ vinyl@^2.2.1: remove-trailing-separator "^1.0.1" replace-ext "^1.0.0" +vscode-arduino-api@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/vscode-arduino-api/-/vscode-arduino-api-0.1.2.tgz#11d294fd72c36bbea1ccacd101f16c11df490b77" + integrity sha512-FxZllcBIUKxYMiakCSOZ2VSaxscQACxzo0tI5xu8HrbDBU5yvl4zvBzwss4PIYvBG0oZeSKDf950i37Qn7dcmA== + dependencies: + "@types/vscode" "^1.78.0" + ardunno-cli "^0.1.2" + safe-stable-stringify "^2.4.3" + vscode-jsonrpc@8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-8.1.0.tgz#cb9989c65e219e18533cc38e767611272d274c94"