diff --git a/arduino-ide-extension/package.json b/arduino-ide-extension/package.json index 1b7cc2de7..7c44dc12d 100644 --- a/arduino-ide-extension/package.json +++ b/arduino-ide-extension/package.json @@ -168,7 +168,7 @@ "version": "14.0.0" }, "languageServer": { - "version": "0.7.2" + "version": "0.7.4" } } } diff --git a/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts b/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts deleted file mode 100644 index 547e44229..000000000 --- a/arduino-ide-extension/src/browser/arduino-workspace-resolver.ts +++ /dev/null @@ -1,68 +0,0 @@ -import { URI } from '@theia/core/shared/vscode-uri'; -import { isWindows } from '@theia/core/lib/common/os'; -import { notEmpty } from '@theia/core/lib/common/objects'; -import { MaybePromise } from '@theia/core/lib/common/types'; - -/** - * Class for determining the default workspace location from the - * `location.hash`, the historical workspace locations, and recent sketch files. - * - * The following logic is used for determining the default workspace location: - * - `hash` points to an existing location? - * - Yes - * - `validate location`. Is valid sketch location? - * - Yes - * - Done. - * - No - * - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`. - * - No - * - `try open recent workspace roots`, then `try open last modified sketches`, finally `create new sketch`. - */ -namespace ArduinoWorkspaceRootResolver { - export interface InitOptions { - readonly isValid: (uri: string) => MaybePromise; - } - export interface ResolveOptions { - readonly hash?: string; - readonly recentWorkspaces: string[]; - // Gathered from the default sketch folder. The default sketch folder is defined by the CLI. - readonly recentSketches: string[]; - } -} -export class ArduinoWorkspaceRootResolver { - constructor(protected options: ArduinoWorkspaceRootResolver.InitOptions) {} - - async resolve( - options: ArduinoWorkspaceRootResolver.ResolveOptions - ): Promise<{ uri: string } | undefined> { - const { hash, recentWorkspaces, recentSketches } = options; - for (const uri of [ - this.hashToUri(hash), - ...recentWorkspaces, - ...recentSketches, - ].filter(notEmpty)) { - const valid = await this.isValid(uri); - if (valid) { - return { uri }; - } - } - return undefined; - } - - protected isValid(uri: string): MaybePromise { - return this.options.isValid(uri); - } - - // Note: here, the `hash` was defined as new `URI(yourValidFsPath).path` so we have to map it to a valid FS path first. - // This is important for Windows only and a NOOP on POSIX. - // Note: we set the `new URI(myValidUri).path.toString()` as the `hash`. See: - // - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L143 and - // - https://github.com/eclipse-theia/theia/blob/8196e9dcf9c8de8ea0910efeb5334a974f426966/packages/workspace/src/browser/workspace-service.ts#L423 - protected hashToUri(hash: string | undefined): string | undefined { - if (hash && hash.length > 1 && hash.startsWith('#')) { - const path = decodeURI(hash.slice(1)).replace(/\\/g, '/'); // Trim the leading `#`, decode the URI and replace Windows separators - return URI.file(path.slice(isWindows && hash.startsWith('/') ? 1 : 0)).toString(); - } - return undefined; - } -} diff --git a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts index 3c968ae23..17ba65ace 100644 --- a/arduino-ide-extension/src/node/arduino-ide-backend-module.ts +++ b/arduino-ide-extension/src/node/arduino-ide-backend-module.ts @@ -113,6 +113,11 @@ import { MessagingContribution } from './theia/core/messaging-contribution'; import { MessagingService } from '@theia/core/lib/node/messaging/messaging-service'; import { HostedPluginReader } from './theia/plugin-ext/plugin-reader'; import { HostedPluginReader as TheiaHostedPluginReader } from '@theia/plugin-ext/lib/hosted/node/plugin-reader'; +import { PluginDeployer } from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { + LocalDirectoryPluginDeployerResolverWithFallback, + PluginDeployer_GH_12064, +} from './theia/plugin-ext/plugin-deployer'; export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(BackendApplication).toSelf().inSingletonScope(); @@ -392,6 +397,12 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { // https://github.com/arduino/arduino-ide/pull/1706#pullrequestreview-1195595080 bind(HostedPluginReader).toSelf().inSingletonScope(); rebind(TheiaHostedPluginReader).toService(HostedPluginReader); + + // https://github.com/eclipse-theia/theia/issues/12064 + bind(LocalDirectoryPluginDeployerResolverWithFallback) + .toSelf() + .inSingletonScope(); + rebind(PluginDeployer).to(PluginDeployer_GH_12064).inSingletonScope(); }); function bindChildLogger(bind: interfaces.Bind, name: string): void { diff --git a/arduino-ide-extension/src/node/theia/plugin-ext/plugin-deployer.ts b/arduino-ide-extension/src/node/theia/plugin-ext/plugin-deployer.ts new file mode 100644 index 000000000..19c46ead1 --- /dev/null +++ b/arduino-ide-extension/src/node/theia/plugin-ext/plugin-deployer.ts @@ -0,0 +1,100 @@ +import { URI } from '@theia/core/lib/common/uri'; +import { + inject, + injectable, + postConstruct, +} from '@theia/core/shared/inversify'; +import { + PluginDeployerResolver, + PluginDeployerResolverContext, +} from '@theia/plugin-ext/lib/common/plugin-protocol'; +import { PluginDeployerImpl } from '@theia/plugin-ext/lib/main/node/plugin-deployer-impl'; +import { LocalDirectoryPluginDeployerResolver } from '@theia/plugin-ext/lib/main/node/resolvers/local-directory-plugin-deployer-resolver'; +import { constants, promises as fs } from 'fs'; +import { isAbsolute, resolve } from 'path'; + +@injectable() +export class LocalDirectoryPluginDeployerResolverWithFallback extends LocalDirectoryPluginDeployerResolver { + override async resolve( + pluginResolverContext: PluginDeployerResolverContext + ): Promise { + const origin = pluginResolverContext.getOriginId(); + // The original implementation must not run when there is a hash in the path. Otherwise, it can resolve an undesired directory. + // Consider app under c:\Users\username\Desktop\# here is my app\ + // Then the flawed logic will incorrectly find c:\Users\username\Desktop location after stripping the rest of the path after the hash. + // The implementation which provides a workaround for the hash in the path assumes that the original Theia logic is correct, when no hash present in the URI path. + let localPath: string | null; + if (origin.includes('#')) { + localPath = await resolveLocalPluginPathFallback( + pluginResolverContext, + this.supportedScheme + ); + } else { + localPath = await this.originalResolveLocalPluginPath( + pluginResolverContext, + this.supportedScheme + ); + } + if (localPath) { + await this.resolveFromLocalPath(pluginResolverContext, localPath); + } + } + + private async originalResolveLocalPluginPath( + context: PluginDeployerResolverContext, + scheme: string + ): Promise { + const object = >this; + if ( + 'resolveLocalPluginPath' in object && + typeof object['resolveLocalPluginPath'] === 'function' + ) { + return object['resolveLocalPluginPath'](context, scheme); + } + return null; + } +} + +async function resolveLocalPluginPathFallback( + context: PluginDeployerResolverContext, + scheme: string +): Promise { + const uri = new URI(context.getOriginId()); + if (uri.scheme === scheme) { + const unencodedRawUri = uri.toString(true); + let fsPath = unencodedRawUri.substring(`${scheme}:`.length); + if (!isAbsolute(fsPath)) { + fsPath = resolve(process.cwd(), fsPath); + } + try { + await fs.access(fsPath, constants.R_OK); + return fsPath; + } catch { + console.warn( + `The local plugin referenced by ${context.getOriginId()} does not exist.` + ); + } + } + return null; +} + +@injectable() +export class PluginDeployer_GH_12064 extends PluginDeployerImpl { + @inject(LocalDirectoryPluginDeployerResolverWithFallback) + private readonly pluginResolver: LocalDirectoryPluginDeployerResolverWithFallback; + + @postConstruct() + protected adjustPluginResolvers(): void { + const pluginResolvers = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + (this as any).pluginResolvers + ); + const index = pluginResolvers.findIndex( + (pluginResolver) => + pluginResolver instanceof LocalDirectoryPluginDeployerResolver + ); + if (index >= 0) { + pluginResolvers.splice(index, 1, this.pluginResolver); + } + } +}