From 4d79caa3de202f05dd3a5468ceacb1e37e8904a0 Mon Sep 17 00:00:00 2001 From: dankeboy36 Date: Sat, 13 May 2023 13:50:41 +0200 Subject: [PATCH 1/3] feat: expose Arduino state to VS Code extensions - Update a shared state on fqbn, port, sketch path, and etc. changes. VS Code extensions can access it and listen on changes. - Force VISX activation order: API VSIX starts first. Signed-off-by: dankeboy36 --- arduino-ide-extension/package.json | 3 +- .../browser/arduino-ide-frontend-module.ts | 2 + .../contributions/update-arduino-state.ts | 179 ++++++++++++++++++ .../browser/theia/plugin-ext/hosted-plugin.ts | 47 ++++- .../common/protocol/arduino-context-mapper.ts | 126 ++++++++++++ .../src/common/protocol/boards-service.ts | 1 + .../src/common/protocol/core-service.ts | 57 +++++- .../src/node/boards-service-impl.ts | 2 + .../src/node/core-service-impl.ts | 100 ++++++++-- .../common/arduino-context-mapper.test.ts | 43 +++++ .../test/node/core-service-impl.slow-test.ts | 5 +- package.json | 1 + yarn.lock | 41 +++- 13 files changed, 569 insertions(+), 38 deletions(-) create mode 100644 arduino-ide-extension/src/browser/contributions/update-arduino-state.ts create mode 100644 arduino-ide-extension/src/common/protocol/arduino-context-mapper.ts create mode 100644 arduino-ide-extension/src/test/common/arduino-context-mapper.test.ts 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..122000941 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,7 @@ 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'; // Hack to fix copy/cut/paste issue after electron version update in Theia. // https://github.com/eclipse-theia/theia/issues/12487 @@ -747,6 +748,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 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/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" From 767cb3deba2fea8b67eb5105131e2cfe702f97f1 Mon Sep 17 00:00:00 2001 From: dankeboy36 Date: Tue, 27 Jun 2023 17:42:49 +0200 Subject: [PATCH 2/3] fix(terminal): widget flickering on resize Ref: eclipse-theia/theia#12587 Signed-off-by: dankeboy36 --- .../browser/arduino-ide-frontend-module.ts | 5 ++++ .../theia/terminal/terminal-widget-impl.ts | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 arduino-ide-extension/src/browser/theia/terminal/terminal-widget-impl.ts 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 122000941..743296fd1 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -355,6 +355,8 @@ 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'; // Hack to fix copy/cut/paste issue after electron version update in Theia. // https://github.com/eclipse-theia/theia/issues/12487 @@ -1026,4 +1028,7 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TheiaMonacoEditorMenuContribution).toService( MonacoEditorMenuContribution ); + + // Patch terminal issues. + rebind(TerminalWidget).to(TerminalWidgetImpl).inTransientScope(); }); 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); + } +} From 18366e2c98ca7099f07ae49b7aae2e09ffa2ec7c Mon Sep 17 00:00:00 2001 From: dankeboy36 Date: Tue, 27 Jun 2023 17:43:49 +0200 Subject: [PATCH 3/3] fix(terminal): `split-terminal` visibility Removed the toolbar contribution from the UI. Ref: eclipse-theia/theia#12626/ Signed-off-by: dankeboy36 --- .../browser/arduino-ide-frontend-module.ts | 4 ++ .../terminal-frontend-contribution.ts | 38 +++++++++++++++++++ 2 files changed, 42 insertions(+) create mode 100644 arduino-ide-extension/src/browser/theia/terminal/terminal-frontend-contribution.ts 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 743296fd1..ea568d595 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -357,6 +357,8 @@ import { MonacoEditorMenuContribution as TheiaMonacoEditorMenuContribution } fro 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 @@ -1031,4 +1033,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // Patch terminal issues. rebind(TerminalWidget).to(TerminalWidgetImpl).inTransientScope(); + bind(TerminalFrontendContribution).toSelf().inSingletonScope(); + rebind(TheiaTerminalFrontendContribution).toService(TerminalFrontendContribution); }); 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; + } +} pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy