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 8152d79dc..434563ef7 100644 --- a/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts +++ b/arduino-ide-extension/src/browser/arduino-ide-frontend-module.ts @@ -67,8 +67,7 @@ import { ListItemRenderer } from './widgets/component-list/list-item-renderer'; import { ColorContribution } from '@theia/core/lib/browser/color-application-contribution'; import { MonacoThemingService } from '@theia/monaco/lib/browser/monaco-theming-service'; import { ArduinoDaemonPath, ArduinoDaemon } from '../common/protocol/arduino-daemon'; -import { EditorManager as TheiaEditorManager, EditorCommandContribution as TheiaEditorCommandContribution } from '@theia/editor/lib/browser'; -import { EditorManager } from './theia/editor/editor-manager'; +import { EditorCommandContribution as TheiaEditorCommandContribution } from '@theia/editor/lib/browser'; import { FrontendConnectionStatusService, ApplicationConnectionStatusContribution } from './theia/core/connection-status-service'; import { FrontendConnectionStatusService as TheiaFrontendConnectionStatusService, @@ -153,6 +152,8 @@ import { SearchInWorkspaceWidget as TheiaSearchInWorkspaceWidget } from '@theia/ import { SearchInWorkspaceWidget } from './theia/search-in-workspace/search-in-workspace-widget'; import { SearchInWorkspaceResultTreeWidget as TheiaSearchInWorkspaceResultTreeWidget } from '@theia/search-in-workspace/lib/browser/search-in-workspace-result-tree-widget'; import { SearchInWorkspaceResultTreeWidget } from './theia/search-in-workspace/search-in-workspace-result-tree-widget'; +import { MonacoEditorProvider } from './theia/monaco/monaco-editor-provider'; +import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; const ElementQueries = require('css-element-queries/src/ElementQueries'); @@ -305,6 +306,8 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { rebind(TheiaOutputChannelRegistryMainImpl).toService(OutputChannelRegistryMainImpl); bind(MonacoTextModelService).toSelf().inSingletonScope(); rebind(TheiaMonacoTextModelService).toService(MonacoTextModelService); + bind(MonacoEditorProvider).toSelf().inSingletonScope(); + rebind(TheiaMonacoEditorProvider).toService(MonacoEditorProvider); bind(SearchInWorkspaceWidget).toSelf(); rebind(TheiaSearchInWorkspaceWidget).toService(SearchInWorkspaceWidget); @@ -321,10 +324,6 @@ export default new ContainerModule((bind, unbind, isBound, rebind) => { bind(FrontendConnectionStatusService).toSelf().inSingletonScope(); rebind(TheiaFrontendConnectionStatusService).toService(FrontendConnectionStatusService); - // Editor customizations. Sets the editor to `readOnly` if under the data dir. - bind(EditorManager).toSelf().inSingletonScope(); - rebind(TheiaEditorManager).toService(EditorManager); - // Decorator customizations bind(TabBarDecoratorService).toSelf().inSingletonScope(); rebind(TheiaTabBarDecoratorService).toService(TabBarDecoratorService); diff --git a/arduino-ide-extension/src/browser/theia/editor/editor-manager.ts b/arduino-ide-extension/src/browser/theia/editor/editor-manager.ts deleted file mode 100644 index 9b87bc3a4..000000000 --- a/arduino-ide-extension/src/browser/theia/editor/editor-manager.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { inject, injectable } from 'inversify'; -import URI from '@theia/core/lib/common/uri'; -import { EditorManager as TheiaEditorManager, EditorOpenerOptions } from '@theia/editor/lib/browser/editor-manager'; -import { ConfigService } from '../../../common/protocol/config-service'; -import { EditorWidget } from '@theia/editor/lib/browser'; -import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; - -@injectable() -export class EditorManager extends TheiaEditorManager { - - @inject(ConfigService) - protected readonly configService: ConfigService; - - async open(uri: URI, options?: EditorOpenerOptions): Promise { - const [widget, readOnly] = await Promise.all([super.open(uri, options), this.isReadOnly(uri)]); - if (readOnly) { - const { editor } = widget; - if (editor instanceof MonacoEditor) { - const codeEditor = editor.getControl(); - codeEditor.updateOptions({ readOnly }); - } - } - return widget; - } - - protected async isReadOnly(uri: URI): Promise { - const config = await this.configService.getConfiguration(); - return new URI(config.dataDirUri).isEqualOrParent(uri) - } - -} diff --git a/arduino-ide-extension/src/browser/theia/monaco/monaco-editor-provider.ts b/arduino-ide-extension/src/browser/theia/monaco/monaco-editor-provider.ts new file mode 100644 index 000000000..c13ca1f1e --- /dev/null +++ b/arduino-ide-extension/src/browser/theia/monaco/monaco-editor-provider.ts @@ -0,0 +1,64 @@ +import { inject, injectable } from 'inversify'; +import URI from '@theia/core/lib/common/uri'; +import { Disposable, DisposableCollection } from '@theia/core/lib/common/disposable'; +import { MonacoEditor } from '@theia/monaco/lib/browser/monaco-editor'; +import { MonacoEditorProvider as TheiaMonacoEditorProvider } from '@theia/monaco/lib/browser/monaco-editor-provider'; +import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl'; + +type CancelablePromise = Promise & { cancel: () => void }; +interface EditorFactory { + (override: monaco.editor.IEditorOverrideServices, toDispose: DisposableCollection): Promise; +} + +@injectable() +export class MonacoEditorProvider extends TheiaMonacoEditorProvider { + + @inject(SketchesServiceClientImpl) + protected readonly sketchesServiceClient: SketchesServiceClientImpl; + + protected async doCreateEditor(uri: URI, factory: EditorFactory): Promise { + const editor = await super.doCreateEditor(uri, factory); + const toDispose = new DisposableCollection(); + toDispose.push(this.installCustomReferencesController(editor)); + toDispose.push(editor.onDispose(() => toDispose.dispose())); + return editor; + } + + private installCustomReferencesController(editor: MonacoEditor): Disposable { + const control = editor.getControl(); + const referencesController = control._contributions['editor.contrib.referencesController']; + const originalToggleWidget = referencesController.toggleWidget; + const toDispose = new DisposableCollection(); + const toDisposeBeforeToggleWidget = new DisposableCollection(); + referencesController.toggleWidget = (range: monaco.Range, modelPromise: CancelablePromise, peekMode: boolean) => { + toDisposeBeforeToggleWidget.dispose(); + originalToggleWidget.bind(referencesController)(range, modelPromise, peekMode); + if (referencesController._widget) { + if ('onDidClose' in referencesController._widget) { + toDisposeBeforeToggleWidget.push((referencesController._widget as any).onDidClose(() => toDisposeBeforeToggleWidget.dispose())); + } + const preview = (referencesController._widget as any)._preview as monaco.editor.ICodeEditor; + if (preview) { + toDisposeBeforeToggleWidget.push(preview.onDidChangeModel(() => this.updateReadOnlyState(preview))) + this.updateReadOnlyState(preview); + } + } + }; + toDispose.push(Disposable.create(() => toDisposeBeforeToggleWidget.dispose())); + toDispose.push(Disposable.create(() => referencesController.toggleWidget = originalToggleWidget)); + return toDispose; + } + + private updateReadOnlyState(editor: monaco.editor.ICodeEditor | undefined): void { + if (!editor) { + return; + } + const model = editor.getModel(); + if (!model) { + return; + } + const readOnly = this.sketchesServiceClient.isReadOnly(model.uri); + editor.updateOptions({ readOnly }); + } + +} diff --git a/arduino-ide-extension/src/browser/theia/monaco/monaco-text-model-service.ts b/arduino-ide-extension/src/browser/theia/monaco/monaco-text-model-service.ts index 1e93b2c30..4a7a46ef7 100644 --- a/arduino-ide-extension/src/browser/theia/monaco/monaco-text-model-service.ts +++ b/arduino-ide-extension/src/browser/theia/monaco/monaco-text-model-service.ts @@ -1,22 +1,29 @@ -import { injectable } from 'inversify'; +import { inject, injectable } from 'inversify'; import { Resource } from '@theia/core/lib/common/resource'; -import { MaybePromise } from '@theia/core/lib/common/types'; -import { Log, Loggable } from '@theia/core/lib/common/logger'; +import { ILogger, Log, Loggable } from '@theia/core/lib/common/logger'; import { MonacoEditorModel } from '@theia/monaco/lib/browser/monaco-editor-model'; +import { EditorPreferences } from '@theia/editor/lib/browser/editor-preferences'; +import { MonacoToProtocolConverter } from '@theia/monaco/lib/browser/monaco-to-protocol-converter'; +import { ProtocolToMonacoConverter } from '@theia/monaco/lib/browser/protocol-to-monaco-converter'; import { MonacoTextModelService as TheiaMonacoTextModelService } from '@theia/monaco/lib/browser/monaco-text-model-service'; +import { SketchesServiceClientImpl } from '../../../common/protocol/sketches-service-client-impl'; @injectable() export class MonacoTextModelService extends TheiaMonacoTextModelService { - protected createModel(resource: Resource): MaybePromise { + @inject(SketchesServiceClientImpl) + protected readonly sketchesServiceClient: SketchesServiceClientImpl; + + protected async createModel(resource: Resource): Promise { const factory = this.factories.getContributions().find(({ scheme }) => resource.uri.scheme === scheme); - return factory ? factory.createModel(resource) : new SilentMonacoEditorModel(resource, this.m2p, this.p2m, this.logger); + const readOnly = this.sketchesServiceClient.isReadOnly(resource.uri); + return factory ? factory.createModel(resource) : new MaybeReadonlyMonacoEditorModel(resource, this.m2p, this.p2m, this.logger, undefined, readOnly); } } // https://github.com/eclipse-theia/theia/pull/8491 -export class SilentMonacoEditorModel extends MonacoEditorModel { +class SilentMonacoEditorModel extends MonacoEditorModel { protected trace(loggable: Loggable): void { if (this.logger) { @@ -27,3 +34,41 @@ export class SilentMonacoEditorModel extends MonacoEditorModel { } } + +class MaybeReadonlyMonacoEditorModel extends SilentMonacoEditorModel { + + constructor( + protected readonly resource: Resource, + protected readonly m2p: MonacoToProtocolConverter, + protected readonly p2m: ProtocolToMonacoConverter, + protected readonly logger?: ILogger, + protected readonly editorPreferences?: EditorPreferences, + protected readonly _readOnly?: boolean, + ) { + super(resource, m2p, p2m, logger, editorPreferences) + } + + get readOnly(): boolean { + if (typeof this._readOnly === 'boolean') { + return this._readOnly; + } + return this.resource.saveContents === undefined; + } + + protected setDirty(dirty: boolean): void { + if (this._readOnly === true) { + // NOOP + return; + } + if (dirty === this._dirty) { + return; + } + this._dirty = dirty; + if (dirty === false) { + (this as any).updateSavedVersionId(); + } + this.onDirtyChangedEmitter.fire(undefined); + } + + +} diff --git a/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts b/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts index dba9a497d..0a6fc0f65 100644 --- a/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts +++ b/arduino-ide-extension/src/common/protocol/sketches-service-client-impl.ts @@ -1,14 +1,15 @@ import { inject, injectable } from 'inversify'; import URI from '@theia/core/lib/common/uri'; +import { Emitter } from '@theia/core/lib/common/event'; import { notEmpty } from '@theia/core/lib/common/objects'; import { FileService } from '@theia/filesystem/lib/browser/file-service'; import { MessageService } from '@theia/core/lib/common/message-service'; +import { FileChangeType } from '@theia/filesystem/lib/common/files'; import { WorkspaceService } from '@theia/workspace/lib/browser/workspace-service'; +import { DisposableCollection } from '@theia/core/lib/common/disposable'; +import { FrontendApplicationContribution } from '@theia/core/lib/browser/frontend-application'; import { Sketch, SketchesService } from '../../common/protocol'; -import { FrontendApplicationContribution } from '@theia/core/lib/browser'; import { ConfigService } from './config-service'; -import { DisposableCollection, Emitter } from '@theia/core'; -import { FileChangeType } from '@theia/filesystem/lib/browser'; import { SketchContainer } from './sketches-service'; @injectable() @@ -129,4 +130,13 @@ export class SketchesServiceClientImpl implements FrontendApplicationContributio }, 100); } + /** + * `true` if the `uri` is not contained in any of the opened workspaces. Otherwise, `false`. + */ + isReadOnly(uri: URI | monaco.Uri | string): boolean { + const toCheck = uri instanceof URI ? uri : new URI(uri); + const readOnly = !this.workspaceService.tryGetRoots().some(({ resource }) => resource.isEqualOrParent(toCheck)); + return readOnly; + } + }