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 6c6b44a63..d2161ebf2 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -356,6 +356,8 @@ import { Account } from './contributions/account'; import { SidebarBottomMenuWidget } from './theia/core/sidebar-bottom-menu-widget'; import { SidebarBottomMenuWidget as TheiaSidebarBottomMenuWidget } from '@theia/core/lib/browser/shell/sidebar-bottom-menu-widget'; import { CreateCloudCopy } from './contributions/create-cloud-copy'; +import { FileResourceResolver } from './theia/filesystem/file-resource'; +import { FileResourceResolver as TheiaFileResourceResolver } from '@theia/filesystem/lib/browser/file-resource'; export default new ContainerModule((bind, unbind, isBound, rebind) => { // Commands and toolbar items @@ -1034,4 +1036,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(FrontendApplicationContribution).toService(DaemonPort); bind(IsOnline).toSelf().inSingletonScope(); bind(FrontendApplicationContribution).toService(IsOnline); + + // https://github.com/arduino/arduino-ide/issues/437 + bind(FileResourceResolver).toSelf().inSingletonScope(); + rebind(TheiaFileResourceResolver).toService(FileResourceResolver); }); diff --git a/arduino-ide-extension/src/browser/theia/filesystem/file-resource.ts b/arduino-ide-extension/src/browser/theia/filesystem/file-resource.ts new file mode 100644 index 000000000..168883910 --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/filesystem/file-resource.ts @@ -0,0 +1,78 @@ +import { ResourceSaveOptions } from '@theia/core/lib/common/resource'; +import { Readable } from '@theia/core/lib/common/stream'; +import URI from '@theia/core/lib/common/uri'; +import { injectable } from '@theia/core/shared/inversify'; +import { + FileResource, + FileResourceOptions, + FileResourceResolver as TheiaFileResourceResolver, +} from '@theia/filesystem/lib/browser/file-resource'; +import { FileService } from '@theia/filesystem/lib/browser/file-service'; +import { + FileOperationError, + FileOperationResult, + FileStat, +} from '@theia/filesystem/lib/common/files'; +import * as PQueue from 'p-queue'; + +@injectable() +export class FileResourceResolver extends TheiaFileResourceResolver { + override async resolve(uri: URI): Promise { + let stat: FileStat | undefined; + try { + stat = await this.fileService.resolve(uri); + } catch (e) { + if ( + !( + e instanceof FileOperationError && + e.fileOperationResult === FileOperationResult.FILE_NOT_FOUND + ) + ) { + throw e; + } + } + if (stat && stat.isDirectory) { + throw new Error( + 'The given uri is a directory: ' + this.labelProvider.getLongName(uri) + ); + } + return new WriteQueuedFileResource(uri, this.fileService, { + shouldOverwrite: () => this.shouldOverwrite(uri), + shouldOpenAsText: (error) => this.shouldOpenAsText(uri, error), + }); + } +} + +class WriteQueuedFileResource extends FileResource { + private readonly writeQueue = new PQueue({ autoStart: true, concurrency: 1 }); + + constructor( + uri: URI, + fileService: FileService, + options: FileResourceOptions + ) { + super(uri, fileService, options); + const originalSaveContentChanges = this['saveContentChanges']; + if (originalSaveContentChanges) { + this['saveContentChanges'] = (changes, options) => { + return this.writeQueue.add(() => + originalSaveContentChanges.bind(this)(changes, options) + ); + }; + } + } + + protected override async doWrite( + content: string | Readable, + options?: ResourceSaveOptions + ): Promise { + return this.writeQueue.add(() => super.doWrite(content, options)); + } + + protected override async isInSync(): Promise { + // Let all the write operations finish to update the version (mtime) before checking whether the resource is in sync. + // https://github.com/eclipse-theia/theia/issues/12327 + await this.writeQueue.onIdle(); + return super.isInSync(); + } +}