diff --git a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts index 563b55da3da..28437a50bab 100644 --- a/src/vs/workbench/api/browser/mainThreadCustomEditors.ts +++ b/src/vs/workbench/api/browser/mainThreadCustomEditors.ts @@ -445,6 +445,8 @@ class MainThreadCustomEditorModel extends ResourceWorkingCopy implements ICustom private readonly _onDidChangeContent: Emitter = this._register(new Emitter()); readonly onDidChangeContent: Event = this._onDidChangeContent.event; + readonly onDidChangeEditable = Event.None; + //#endregion public isEditable(): boolean { diff --git a/src/vs/workbench/browser/parts/editor/editorGroupView.ts b/src/vs/workbench/browser/parts/editor/editorGroupView.ts index ff1a771f5dd..fe11ee21bad 100644 --- a/src/vs/workbench/browser/parts/editor/editorGroupView.ts +++ b/src/vs/workbench/browser/parts/editor/editorGroupView.ts @@ -504,7 +504,8 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._register(this.model.onDidCloseEditor(editor => this.handleOnDidCloseEditor(editor))); this._register(this.model.onWillDisposeEditor(editor => this.onWillDisposeEditor(editor))); this._register(this.model.onDidChangeEditorDirty(editor => this.onDidChangeEditorDirty(editor))); - this._register(this.model.onDidEditorLabelChange(editor => this.onDidEditorLabelChange(editor))); + this._register(this.model.onDidChangeEditorLabel(editor => this.onDidChangeEditorLabel(editor))); + this._register(this.model.onDidChangeEditorCapabilities(editor => this.onDidChangeEditorCapabilities(editor))); // Option Changes this._register(this.accessor.onDidChangeEditorPartOptions(e => this.onDidChangeEditorPartOptions(e))); @@ -692,7 +693,7 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_DIRTY, editor }); } - private onDidEditorLabelChange(editor: EditorInput): void { + private onDidChangeEditorLabel(editor: EditorInput): void { // Forward to title control this.titleAreaControl.updateEditorLabel(editor); @@ -701,6 +702,15 @@ export class EditorGroupView extends Themable implements IEditorGroupView { this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_LABEL, editor }); } + private onDidChangeEditorCapabilities(editor: EditorInput): void { + + // Forward to title control + this.titleAreaControl.updateEditorCapabilities(editor); + + // Event + this._onDidGroupChange.fire({ kind: GroupChangeKind.EDITOR_CAPABILITIES, editor }); + } + private onDidVisibilityChange(visible: boolean): void { // Forward to editor control diff --git a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts index d2fb9bf8ce7..d61ed38725e 100644 --- a/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/noTabsTitleControl.ts @@ -167,6 +167,10 @@ export class NoTabsTitleControl extends TitleControl { this.ifEditorIsActive(editor, () => this.redraw()); } + updateEditorCapabilities(editor: IEditorInput): void { + this.ifEditorIsActive(editor, () => this.redraw()); + } + updateEditorLabels(): void { if (this.group.activeEditor) { this.updateEditorLabel(this.group.activeEditor); // we only have the active one to update diff --git a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts index 464af3eee27..ae94f70f7a2 100644 --- a/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts +++ b/src/vs/workbench/browser/parts/editor/tabsTitleControl.ts @@ -509,6 +509,10 @@ export class TabsTitleControl extends TitleControl { this.layout(this.dimensions); } + updateEditorCapabilities(editor: IEditorInput): void { + this.updateEditorLabel(editor); + } + private updateEditorLabelAggregator = this._register(new RunOnceScheduler(() => this.updateEditorLabels(), 0)); updateEditorLabel(editor: IEditorInput): void { diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 5e723df1bc0..4e5aee431f9 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -22,7 +22,7 @@ import { IInstantiationService } from 'vs/platform/instantiation/common/instanti import { IThemeService } from 'vs/platform/theme/common/themeService'; import { TextFileOperationError, TextFileOperationResult } from 'vs/workbench/services/textfile/common/textfiles'; import { ScrollType, IDiffEditorViewState, IDiffEditorModel } from 'vs/editor/common/editorCommon'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import { Registry } from 'vs/platform/registry/common/platform'; import { URI } from 'vs/base/common/uri'; import { IEditorGroupsService } from 'vs/workbench/services/editor/common/editorGroupsService'; @@ -44,6 +44,8 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan private diffNavigator: DiffNavigator | undefined; private readonly diffNavigatorDisposables = this._register(new DisposableStore()); + private readonly inputListener = this._register(new MutableDisposable()); + override get scopedContextKeyService(): IContextKeyService | undefined { const control = this.getControl(); if (!control) { @@ -69,21 +71,29 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan super(TextDiffEditor.ID, telemetryService, instantiationService, storageService, configurationService, themeService, editorService, editorGroupService); // Listen to file system provider changes - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidFileSystemProviderChange(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidFileSystemProviderChange(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); } - private onDidFileSystemProviderChange(scheme: string): void { - const control = this.getControl(); - const input = this.input; + private onDidChangeFileSystemProvider(scheme: string): void { + if (this.input instanceof DiffEditorInput && (this.input.originalInput.resource?.scheme === scheme || this.input.modifiedInput.resource?.scheme === scheme)) { + this.updateReadonly(this.input); + } + } - if (control && input instanceof DiffEditorInput) { - if (input.originalInput.resource?.scheme === scheme || input.modifiedInput.resource?.scheme === scheme) { - control.updateOptions({ - readOnly: input.modifiedInput.hasCapability(EditorInputCapabilities.Readonly), - originalEditable: !input.originalInput.hasCapability(EditorInputCapabilities.Readonly) - }); - } + private onDidChangeInputCapabilities(input: DiffEditorInput): void { + if (this.input === input) { + this.updateReadonly(input); + } + } + + private updateReadonly(input: DiffEditorInput): void { + const control = this.getControl(); + if (control) { + control.updateOptions({ + readOnly: input.modifiedInput.hasCapability(EditorInputCapabilities.Readonly), + originalEditable: !input.originalInput.hasCapability(EditorInputCapabilities.Readonly) + }); } } @@ -109,6 +119,9 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan override async setInput(input: DiffEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + // Update our listener for input capabilities + this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); + // Dispose previous diff navigator this.diffNavigatorDisposables.clear(); @@ -268,6 +281,9 @@ export class TextDiffEditor extends BaseTextEditor implements ITextDiffEditorPan override clearInput(): void { + // Clear input listener + this.inputListener.clear(); + // Dispose previous diff navigator this.diffNavigatorDisposables.clear(); diff --git a/src/vs/workbench/browser/parts/editor/titleControl.ts b/src/vs/workbench/browser/parts/editor/titleControl.ts index 8f73bcf4a26..3de99faa119 100644 --- a/src/vs/workbench/browser/parts/editor/titleControl.ts +++ b/src/vs/workbench/browser/parts/editor/titleControl.ts @@ -381,6 +381,8 @@ export abstract class TitleControl extends Themable { abstract updateEditorLabel(editor: IEditorInput): void; + abstract updateEditorCapabilities(editor: IEditorInput): void; + abstract updateEditorLabels(): void; abstract updateEditorDirty(editor: IEditorInput): void; diff --git a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts index 8572b7f405f..d00e11c292c 100644 --- a/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts +++ b/src/vs/workbench/browser/parts/titlebar/titlebarPart.ts @@ -166,6 +166,7 @@ export class TitlebarPart extends Part implements ITitleService { if (activeEditor) { this.activeEditorListeners.add(activeEditor.onDidChangeDirty(() => this.titleUpdater.schedule())); this.activeEditorListeners.add(activeEditor.onDidChangeLabel(() => this.titleUpdater.schedule())); + this.activeEditorListeners.add(activeEditor.onDidChangeCapabilities(() => this.titleUpdater.schedule())); } } diff --git a/src/vs/workbench/common/editor.ts b/src/vs/workbench/common/editor.ts index a34e3200aef..c2ac8757b10 100644 --- a/src/vs/workbench/common/editor.ts +++ b/src/vs/workbench/common/editor.ts @@ -421,6 +421,11 @@ export interface IEditorInput extends IDisposable { */ readonly onDidChangeLabel: Event; + /** + * Triggered when this input changes its capabilities. + */ + readonly onDidChangeCapabilities: Event; + /** * Unique type identifier for this input. Every editor input of the * same class should share the same type identifier. The type identifier diff --git a/src/vs/workbench/common/editor/editorGroupModel.ts b/src/vs/workbench/common/editor/editorGroupModel.ts index bb835b762b0..ec7c3b4ca39 100644 --- a/src/vs/workbench/common/editor/editorGroupModel.ts +++ b/src/vs/workbench/common/editor/editorGroupModel.ts @@ -82,7 +82,10 @@ export class EditorGroupModel extends Disposable { readonly onDidChangeEditorDirty = this._onDidChangeEditorDirty.event; private readonly _onDidChangeEditorLabel = this._register(new Emitter()); - readonly onDidEditorLabelChange = this._onDidChangeEditorLabel.event; + readonly onDidChangeEditorLabel = this._onDidChangeEditorLabel.event; + + private readonly _onDidChangeEditorCapabilities = this._register(new Emitter()); + readonly onDidChangeEditorCapabilities = this._onDidChangeEditorCapabilities.event; private readonly _onDidMoveEditor = this._register(new Emitter()); readonly onDidMoveEditor = this._onDidMoveEditor.event; @@ -333,6 +336,11 @@ export class EditorGroupModel extends Disposable { this._onDidChangeEditorLabel.fire(editor); })); + // Re-Emit capability changes + listeners.add(editor.onDidChangeCapabilities(() => { + this._onDidChangeEditorCapabilities.fire(editor); + })); + // Clean up dispose listeners once the editor gets closed listeners.add(this.onDidCloseEditor(event => { if (event.editor.matches(editor)) { diff --git a/src/vs/workbench/common/editor/editorInput.ts b/src/vs/workbench/common/editor/editorInput.ts index 719b12c85f0..82250013fff 100644 --- a/src/vs/workbench/common/editor/editorInput.ts +++ b/src/vs/workbench/common/editor/editorInput.ts @@ -22,6 +22,9 @@ export abstract class EditorInput extends Disposable implements IEditorInput { protected readonly _onDidChangeLabel = this._register(new Emitter()); readonly onDidChangeLabel = this._onDidChangeLabel.event; + protected readonly _onDidChangeCapabilities = this._register(new Emitter()); + readonly onDidChangeCapabilities = this._onDidChangeCapabilities.event; + private readonly _onWillDispose = this._register(new Emitter()); readonly onWillDispose = this._onWillDispose.event; diff --git a/src/vs/workbench/common/editor/sideBySideEditorInput.ts b/src/vs/workbench/common/editor/sideBySideEditorInput.ts index edd8cc12d74..6eb10a4f271 100644 --- a/src/vs/workbench/common/editor/sideBySideEditorInput.ts +++ b/src/vs/workbench/common/editor/sideBySideEditorInput.ts @@ -81,9 +81,13 @@ export class SideBySideEditorInput extends EditorInput implements ISideBySideEdi } })); - // Reemit some events from the primary side to the outside + // Re-emit some events from the primary side to the outside this._register(this.primary.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this._register(this.primary.onDidChangeLabel(() => this._onDidChangeLabel.fire())); + + // Re-emit some events from both sides to the outside + this._register(this.primary.onDidChangeCapabilities(() => this._onDidChangeCapabilities.fire())); + this._register(this.secondary.onDidChangeCapabilities(() => this._onDidChangeCapabilities.fire())); } override getName(): string { diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index c7e320798f9..ed23e79dbac 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -226,6 +226,7 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { this._modelRef = this._register(assertIsDefined(await this.customEditorService.models.tryRetain(this.resource, this.viewType))); this._register(this._modelRef.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this._register(this._modelRef.object.onDidChangeOrphaned(() => this._onDidChangeLabel.fire())); + this._register(this._modelRef.object.onDidChangeEditable(() => this._onDidChangeCapabilities.fire())); // If we're loading untitled file data we should ensure it's dirty if (this._untitledDocumentData) { this._defaultDirtyState = true; diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index bfaa0a181f8..2b385de28bb 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -58,6 +58,7 @@ export interface ICustomEditorModel extends IDisposable { readonly backupId: string | undefined; isEditable(): boolean; + readonly onDidChangeEditable: Event; isOnReadonlyFileSystem(): boolean; isOrphaned(): boolean; diff --git a/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts index c39fa2ce52c..2f03131d716 100644 --- a/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts @@ -33,6 +33,9 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo private readonly _onDidChangeOrphaned = this._register(new Emitter()); public readonly onDidChangeOrphaned = this._onDidChangeOrphaned.event; + private readonly _onDidChangeEditable = this._register(new Emitter()); + public readonly onDidChangeEditable = this._onDidChangeEditable.event; + constructor( public readonly viewType: string, private readonly _resource: URI, @@ -47,6 +50,7 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo this._textFileModel = this.textFileService.files.get(_resource); if (this._textFileModel) { this._register(this._textFileModel.onDidChangeOrphaned(() => this._onDidChangeOrphaned.fire())); + this._register(this._textFileModel.onDidChangeReadonly(() => this._onDidChangeEditable.fire())); } this._register(this.textFileService.files.onDidChangeDirty(e => { diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts index 71406a52fd2..a7f12ed1c8f 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts @@ -134,7 +134,7 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements // re-emit some events from the model this.modelListeners.add(model.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this.modelListeners.add(model.onDidChangeOrphaned(() => this._onDidChangeLabel.fire())); - this.modelListeners.add(model.onDidChangeReadonly(() => this._onDidChangeLabel.fire())); + this.modelListeners.add(model.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); // important: treat save errors as potential dirty change because // a file that is in save conflict or error will report dirty even diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index 26c09dfc41a..6e670540442 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -32,6 +32,7 @@ import { createErrorWithActions } from 'vs/base/common/errors'; import { EditorActivation, EditorOverride, ITextEditorOptions } from 'vs/platform/editor/common/editor'; import { IUriIdentityService } from 'vs/workbench/services/uriIdentity/common/uriIdentity'; import { IExplorerService } from 'vs/workbench/contrib/files/browser/files'; +import { MutableDisposable } from 'vs/base/common/lifecycle'; /** * An implementation of editor for file system resources. @@ -40,6 +41,8 @@ export class TextFileEditor extends BaseTextEditor { static readonly ID = TEXT_FILE_EDITOR_ID; + private readonly inputListener = this._register(new MutableDisposable()); + constructor( @ITelemetryService telemetryService: ITelemetryService, @IFileService private readonly fileService: IFileService, @@ -64,8 +67,8 @@ export class TextFileEditor extends BaseTextEditor { this._register(this.fileService.onDidRunOperation(e => this.onDidRunOperation(e))); // Listen to file system provider changes - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidFileSystemProviderChange(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidFileSystemProviderChange(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); } private onDidFilesChange(e: FileChangesEvent): void { @@ -81,10 +84,21 @@ export class TextFileEditor extends BaseTextEditor { } } - private onDidFileSystemProviderChange(scheme: string): void { + private onDidChangeFileSystemProvider(scheme: string): void { + if (this.input?.resource.scheme === scheme) { + this.updateReadonly(this.input); + } + } + + private onDidChangeInputCapabilities(input: FileEditorInput): void { + if (this.input === input) { + this.updateReadonly(input); + } + } + + private updateReadonly(input: FileEditorInput): void { const control = this.getControl(); - const input = this.input; - if (control && input?.resource.scheme === scheme) { + if (control) { control.updateOptions({ readOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); } } @@ -107,6 +121,9 @@ export class TextFileEditor extends BaseTextEditor { override async setInput(input: FileEditorInput, options: ITextEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { + // Update our listener for input capabilities + this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); + // Update/clear view settings if input changes this.doSaveOrClearTextEditorViewState(this.input); @@ -229,6 +246,9 @@ export class TextFileEditor extends BaseTextEditor { override clearInput(): void { + // Clear input listener + this.inputListener.clear(); + // Update/clear editor view state in settings this.doSaveOrClearTextEditorViewState(this.input); diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index e0d597d7889..d8176ad5cff 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -163,6 +163,7 @@ export class OpenEditorsView extends ViewPane { } case GroupChangeKind.EDITOR_DIRTY: case GroupChangeKind.EDITOR_LABEL: + case GroupChangeKind.EDITOR_CAPABILITIES: case GroupChangeKind.EDITOR_STICKY: case GroupChangeKind.EDITOR_PIN: { this.list.splice(index, 1, [new OpenEditor(e.editor!, group)]); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index de58dd26bc7..f88c3f92d2d 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -6,7 +6,7 @@ import * as DOM from 'vs/base/browser/dom'; import { CancellationToken } from 'vs/base/common/cancellation'; import { Emitter, Event } from 'vs/base/common/event'; -import { DisposableStore } from 'vs/base/common/lifecycle'; +import { DisposableStore, MutableDisposable } from 'vs/base/common/lifecycle'; import 'vs/css!./media/notebook'; import { localize } from 'vs/nls'; import { extname } from 'vs/base/common/resources'; @@ -46,6 +46,8 @@ export class NotebookEditor extends EditorPane { private _rootElement!: HTMLElement; private _dimension?: DOM.Dimension; + private readonly inputListener = this._register(new MutableDisposable()); + // todo@rebornix is there a reason that `super.fireOnDidFocus` isn't used? private readonly _onDidFocusWidget = this._register(new Emitter()); override get onDidFocus(): Event { return this._onDidFocusWidget.event; } @@ -69,13 +71,25 @@ export class NotebookEditor extends EditorPane { super(NotebookEditor.ID, telemetryService, themeService, storageService); this._editorMemento = this.getEditorMemento(_editorGroupService, NOTEBOOK_EDITOR_VIEW_STATE_PREFERENCE_KEY); - this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidFileSystemProviderChange(e.scheme))); - this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidFileSystemProviderChange(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProvider(e.scheme))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProvider(e.scheme))); } - private onDidFileSystemProviderChange(scheme: string): void { - if (this.input?.resource?.scheme === scheme && this._widget.value) { - this._widget.value.setOptions({ isReadOnly: this.input.hasCapability(EditorInputCapabilities.Readonly) }); + private onDidChangeFileSystemProvider(scheme: string): void { + if (this.input instanceof NotebookEditorInput && this.input.resource?.scheme === scheme) { + this.updateReadonly(this.input); + } + } + + private onDidChangeInputCapabilities(input: NotebookEditorInput): void { + if (this.input === input) { + this.updateReadonly(input); + } + } + + private updateReadonly(input: NotebookEditorInput): void { + if (this._widget.value) { + this._widget.value.setOptions({ isReadOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); } } @@ -157,6 +171,8 @@ export class NotebookEditor extends EditorPane { mark(input.resource, 'startTime'); const group = this.group!; + this.inputListener.value = input.onDidChangeCapabilities(() => this.onDidChangeInputCapabilities(input)); + this._saveEditorViewState(this.input); this._widgetDisposableStore.clear(); @@ -270,6 +286,8 @@ export class NotebookEditor extends EditorPane { } override clearInput(): void { + this.inputListener.clear(); + if (this._widget.value) { this._saveEditorViewState(this.input); this._widget.value.onWillHide(); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 50bc8a479c7..ed4efbd3d89 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -189,7 +189,7 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { } this._register(this._editorModelReference.object.onDidChangeDirty(() => this._onDidChangeDirty.fire())); this._register(this._editorModelReference.object.onDidChangeOrphaned(() => this._onDidChangeLabel.fire())); - this._register(this._editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeLabel.fire())); + this._register(this._editorModelReference.object.onDidChangeReadonly(() => this._onDidChangeCapabilities.fire())); if (this._editorModelReference.object.isDirty()) { this._onDidChangeDirty.fire(); } diff --git a/src/vs/workbench/services/editor/common/editorGroupsService.ts b/src/vs/workbench/services/editor/common/editorGroupsService.ts index 43c29c5c77f..f7283b6c246 100644 --- a/src/vs/workbench/services/editor/common/editorGroupsService.ts +++ b/src/vs/workbench/services/editor/common/editorGroupsService.ts @@ -372,6 +372,7 @@ export const enum GroupChangeKind { EDITOR_MOVE, EDITOR_ACTIVE, EDITOR_LABEL, + EDITOR_CAPABILITIES, EDITOR_PIN, EDITOR_STICKY, EDITOR_DIRTY diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts index c98a79f6c3f..82fcdb7212f 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModel.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModel.ts @@ -10,7 +10,7 @@ import { EncodingMode, ITextFileService, TextFileEditorModelState, ITextFileEdit import { IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel'; import { IWorkingCopyBackupService, IResolvedWorkingCopyBackup } from 'vs/workbench/services/workingCopy/common/workingCopyBackup'; -import { IFileService, FileOperationError, FileOperationResult, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED, FileSystemProviderCapabilities } from 'vs/platform/files/common/files'; +import { IFileService, FileOperationError, FileOperationResult, FileChangesEvent, FileChangeType, IFileStatWithMetadata, ETAG_DISABLED, FileSystemProviderCapabilities, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { IModeService } from 'vs/editor/common/services/modeService'; import { IModelService } from 'vs/editor/common/services/modelService'; import { timeout, TaskSequentializer } from 'vs/base/common/async'; @@ -437,8 +437,13 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND); // NotModified status is expected and can be handled gracefully - // if we are resolved + // if we are resolved. We still want to update our last resolved + // stat to e.g. detect changes to the file's readonly state if (this.isResolved() && result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { + if (error instanceof NotModifiedSinceFileOperationError) { + this.updateLastResolvedFileStat(error.stat); + } + return; } diff --git a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts index efa07ebc54e..5d7ba1844d2 100644 --- a/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts +++ b/src/vs/workbench/services/textfile/common/textFileEditorModelManager.ts @@ -12,7 +12,7 @@ import { dispose, IDisposable, Disposable, DisposableStore } from 'vs/base/commo import { ITextFileEditorModel, ITextFileEditorModelManager, ITextFileEditorModelResolveOrCreateOptions, ITextFileResolveEvent, ITextFileSaveEvent, ITextFileSaveParticipant } from 'vs/workbench/services/textfile/common/textfiles'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { ResourceMap } from 'vs/base/common/map'; -import { IFileService, FileChangesEvent, FileOperation, FileChangeType } from 'vs/platform/files/common/files'; +import { IFileService, FileChangesEvent, FileOperation, FileChangeType, IFileSystemProviderRegistrationEvent, IFileSystemProviderCapabilitiesChangeEvent } from 'vs/platform/files/common/files'; import { Promises, ResourceQueue } from 'vs/base/common/async'; import { onUnexpectedError } from 'vs/base/common/errors'; import { TextFileSaveParticipant } from 'vs/workbench/services/textfile/common/textFileSaveParticipant'; @@ -87,6 +87,10 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE // Update models from file change events this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); + // File system provider changes + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProviderCapabilities(e))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProviderRegistrations(e))); + // Working copy operations this._register(this.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => this.onWillRunWorkingCopyFileOperation(e))); this._register(this.workingCopyFileService.onDidFailWorkingCopyFileOperation(e => this.onDidFailWorkingCopyFileOperation(e))); @@ -108,6 +112,39 @@ export class TextFileEditorModelManager extends Disposable implements ITextFileE } } + private onDidChangeFileSystemProviderCapabilities(e: IFileSystemProviderCapabilitiesChangeEvent): void { + + // Resolve models again for file systems that changed + // capabilities to fetch latest metadata (e.g. readonly) + // into all models. + this.queueModelResolves(e.scheme); + } + + private onDidChangeFileSystemProviderRegistrations(e: IFileSystemProviderRegistrationEvent): void { + if (!e.added) { + return; // only if added + } + + // Resolve models again for file systems that registered + // to account for capability changes: extensions may + // unregister and register the same provider with different + // capabilities, so we want to ensure to fetch latest + // metadata (e.g. readonly) into all models. + this.queueModelResolves(e.scheme); + } + + private queueModelResolves(scheme: string): void { + for (const model of this.models) { + if (model.isDirty() || !model.isResolved()) { + continue; // require a resolved, saved model to continue + } + + if (scheme === model.resource.scheme) { + this.queueModelResolve(model); + } + } + } + private queueModelResolve(model: TextFileEditorModel): void { // Resolve model to update (use a queue to prevent accumulation of resolves diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts index b5a97afc967..06bff7995cc 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopy.ts @@ -7,7 +7,7 @@ import { localize } from 'vs/nls'; import { URI } from 'vs/base/common/uri'; import { Event, Emitter } from 'vs/base/common/event'; import { CancellationToken, CancellationTokenSource } from 'vs/base/common/cancellation'; -import { ETAG_DISABLED, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IFileService, IFileStatWithMetadata, IFileStreamContent, IWriteFileOptions } from 'vs/platform/files/common/files'; +import { ETAG_DISABLED, FileOperationError, FileOperationResult, FileSystemProviderCapabilities, IFileService, IFileStatWithMetadata, IFileStreamContent, IWriteFileOptions, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { ISaveOptions, IRevertOptions, SaveReason } from 'vs/workbench/common/editor'; import { IWorkingCopyService } from 'vs/workbench/services/workingCopy/common/workingCopyService'; import { IWorkingCopyBackup, IWorkingCopyBackupMeta, WorkingCopyCapabilities } from 'vs/workbench/services/workingCopy/common/workingCopy'; @@ -549,8 +549,13 @@ export class StoredFileWorkingCopy extend this.setOrphaned(result === FileOperationResult.FILE_NOT_FOUND); // NotModified status is expected and can be handled gracefully - // if we are resolved + // if we are resolved. We still want to update our last resolved + // stat to e.g. detect changes to the file's readonly state if (this.isResolved() && result === FileOperationResult.FILE_NOT_MODIFIED_SINCE) { + if (error instanceof NotModifiedSinceFileOperationError) { + this.updateLastResolvedFileStat(error.stat); + } + return; } diff --git a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts index 24b22e5681a..073089781b8 100644 --- a/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts +++ b/src/vs/workbench/services/workingCopy/common/storedFileWorkingCopyManager.ts @@ -9,7 +9,7 @@ import { StoredFileWorkingCopy, StoredFileWorkingCopyState, IStoredFileWorkingCo import { SaveReason } from 'vs/workbench/common/editor'; import { ResourceMap } from 'vs/base/common/map'; import { Promises, ResourceQueue } from 'vs/base/common/async'; -import { FileChangesEvent, FileChangeType, FileOperation, IFileService } from 'vs/platform/files/common/files'; +import { FileChangesEvent, FileChangeType, FileOperation, IFileService, IFileSystemProviderCapabilitiesChangeEvent, IFileSystemProviderRegistrationEvent } from 'vs/platform/files/common/files'; import { ILifecycleService } from 'vs/workbench/services/lifecycle/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import { VSBufferReadableStream } from 'vs/base/common/buffer'; @@ -171,6 +171,10 @@ export class StoredFileWorkingCopyManager // Update working copies from file change events this._register(this.fileService.onDidFilesChange(e => this.onDidFilesChange(e))); + // File system provider changes + this._register(this.fileService.onDidChangeFileSystemProviderCapabilities(e => this.onDidChangeFileSystemProviderCapabilities(e))); + this._register(this.fileService.onDidChangeFileSystemProviderRegistrations(e => this.onDidChangeFileSystemProviderRegistrations(e))); + // Working copy operations this._register(this.workingCopyFileService.onWillRunWorkingCopyFileOperation(e => this.onWillRunWorkingCopyFileOperation(e))); this._register(this.workingCopyFileService.onDidFailWorkingCopyFileOperation(e => this.onDidFailWorkingCopyFileOperation(e))); @@ -191,19 +195,54 @@ export class StoredFileWorkingCopyManager } } - //#region Resolve from file changes + //#region Resolve from file or file provider changes + + private onDidChangeFileSystemProviderCapabilities(e: IFileSystemProviderCapabilitiesChangeEvent): void { + + // Resolve working copies again for file systems that changed + // capabilities to fetch latest metadata (e.g. readonly) + // into all working copies. + this.queueWorkingCopyResolves(e.scheme); + } + + private onDidChangeFileSystemProviderRegistrations(e: IFileSystemProviderRegistrationEvent): void { + if (!e.added) { + return; // only if added + } + + // Resolve working copies again for file systems that registered + // to account for capability changes: extensions may unregister + // and register the same provider with different capabilities, + // so we want to ensure to fetch latest metadata (e.g. readonly) + // into all working copies. + this.queueWorkingCopyResolves(e.scheme); + } private onDidFilesChange(e: FileChangesEvent): void { + + // Trigger a resolve for any update or add event that impacts + // the working copy. We also consider the added event + // because it could be that a file was added and updated + // right after. + this.queueWorkingCopyResolves(e); + } + + private queueWorkingCopyResolves(scheme: string): void; + private queueWorkingCopyResolves(e: FileChangesEvent): void; + private queueWorkingCopyResolves(schemeOrEvent: string | FileChangesEvent): void { for (const workingCopy of this.workingCopies) { if (workingCopy.isDirty() || !workingCopy.isResolved()) { continue; // require a resolved, saved working copy to continue } - // Trigger a resolve for any update or add event that impacts - // the working copy. We also consider the added event - // because it could be that a file was added and updated - // right after. - if (e.contains(workingCopy.resource, FileChangeType.UPDATED, FileChangeType.ADDED)) { + let resolveWorkingCopy = false; + if (typeof schemeOrEvent === 'string') { + resolveWorkingCopy = schemeOrEvent === workingCopy.resource.scheme; + } else { + resolveWorkingCopy = schemeOrEvent.contains(workingCopy.resource, FileChangeType.UPDATED, FileChangeType.ADDED); + } + + if (resolveWorkingCopy) { this.queueWorkingCopyResolve(workingCopy); } } diff --git a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts index c135244e3ec..f33092657f1 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -13,7 +13,7 @@ import { Disposable } from 'vs/base/common/lifecycle'; import { TestServiceAccessor, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; import { basename } from 'vs/base/common/resources'; -import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult } from 'vs/platform/files/common/files'; +import { FileChangesEvent, FileChangeType, FileOperationError, FileOperationResult, NotModifiedSinceFileOperationError } from 'vs/platform/files/common/files'; import { SaveReason } from 'vs/workbench/common/editor'; import { Promises } from 'vs/base/common/async'; import { consumeReadable, consumeStream, isReadableStream } from 'vs/base/common/stream'; @@ -348,6 +348,37 @@ suite('StoredFileWorkingCopy', function () { assert.strictEqual(workingCopy.model?.contents, 'Hello Html'); }); + test('resolve (FILE_NOT_MODIFIED_SINCE still updates readonly state)', async () => { + let readonlyChangeCounter = 0; + workingCopy.onDidChangeReadonly(() => readonlyChangeCounter++); + + await workingCopy.resolve(); + + assert.strictEqual(workingCopy.isReadonly(), false); + + const stat = await accessor.fileService.resolve(workingCopy.resource, { resolveMetadata: true }); + + try { + accessor.fileService.readShouldThrowError = new NotModifiedSinceFileOperationError('file not modified since', { ...stat, readonly: true }); + await workingCopy.resolve(); + } finally { + accessor.fileService.readShouldThrowError = undefined; + } + + assert.strictEqual(workingCopy.isReadonly(), true); + assert.strictEqual(readonlyChangeCounter, 1); + + try { + accessor.fileService.readShouldThrowError = new NotModifiedSinceFileOperationError('file not modified since', { ...stat, readonly: false }); + await workingCopy.resolve(); + } finally { + accessor.fileService.readShouldThrowError = undefined; + } + + assert.strictEqual(workingCopy.isReadonly(), false); + assert.strictEqual(readonlyChangeCounter, 2); + }); + test('resolve does not alter content when model content changed in parallel', async () => { await workingCopy.resolve(); diff --git a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts index ad6e444523a..7ce3f04d12a 100644 --- a/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editorGroupModel.test.ts @@ -258,7 +258,7 @@ suite('Workbench editor group model', () => { assert.strictEqual(clone.count, 3); let didEditorLabelChange = false; - const toDispose = clone.onDidEditorLabelChange(() => didEditorLabelChange = true); + const toDispose = clone.onDidChangeEditorLabel(() => didEditorLabelChange = true); input1.setLabel(); assert.ok(didEditorLabelChange); @@ -1560,12 +1560,12 @@ suite('Workbench editor group model', () => { }); let label1ChangeCounter = 0; - group1.onDidEditorLabelChange(() => { + group1.onDidChangeEditorLabel(() => { label1ChangeCounter++; }); let label2ChangeCounter = 0; - group2.onDidEditorLabelChange(() => { + group2.onDidChangeEditorLabel(() => { label2ChangeCounter++; });