From 7190530d358964003196389d2112c6264ff50f48 Mon Sep 17 00:00:00 2001 From: Benjamin Pasero Date: Wed, 21 Jun 2023 14:40:17 +0200 Subject: [PATCH] readonly - adopt `readOnlyMessage` for readonly mode (#185756) --- build/lib/i18n.resources.json | 4 ++ src/vs/platform/files/common/files.ts | 15 ++++++- src/vs/workbench/browser/contextkeys.ts | 2 +- .../browser/parts/editor/editorAutoSave.ts | 2 +- .../browser/parts/editor/editorStatus.ts | 10 ++--- .../parts/editor/editorWithViewState.ts | 7 --- .../browser/parts/editor/textDiffEditor.ts | 14 +++--- .../browser/parts/editor/textEditor.ts | 15 +++++-- .../parts/editor/textResourceEditor.ts | 2 +- src/vs/workbench/common/editor/editorInput.ts | 2 +- .../common/editor/resourceEditorInput.ts | 5 +++ .../common/editor/sideBySideEditorInput.ts | 5 +++ .../customEditor/browser/customEditorInput.ts | 8 ++++ .../customEditor/common/customEditor.ts | 3 +- .../common/customTextEditorModel.ts | 5 ++- .../files/browser/editors/fileEditorInput.ts | 4 +- .../files/browser/editors/textFileEditor.ts | 2 +- .../files/browser/views/explorerView.ts | 2 +- .../files/browser/views/openEditorsView.ts | 4 +- .../contrib/files/common/explorerModel.ts | 5 ++- .../test/browser/fileEditorInput.test.ts | 8 +++- .../notebook/browser/notebookEditor.ts | 6 +-- .../notebook/common/notebookEditorInput.ts | 8 ++++ .../common/filesConfigurationService.ts | 43 +++++++++++++------ .../test/browser/untitledTextEditor.test.ts | 1 + .../browser/storedFileWorkingCopy.test.ts | 4 +- .../test/browser/parts/editor/editor.test.ts | 5 +++ .../parts/editor/resourceEditorInput.test.ts | 1 + 28 files changed, 134 insertions(+), 58 deletions(-) diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index d684f4d945e..63f5497bebe 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -382,6 +382,10 @@ "name": "vs/workbench/services/files", "project": "vscode-workbench" }, + { + "name": "vs/workbench/services/filesConfiguration", + "project": "vscode-workbench" + }, { "name": "vs/workbench/services/history", "project": "vscode-workbench" diff --git a/src/vs/platform/files/common/files.ts b/src/vs/platform/files/common/files.ts index 0f15e61fb0e..4f7c149b6d6 100644 --- a/src/vs/platform/files/common/files.ts +++ b/src/vs/platform/files/common/files.ts @@ -584,7 +584,6 @@ export const enum FileSystemProviderCapabilities { export interface IFileSystemProvider { readonly capabilities: FileSystemProviderCapabilities; - readonly readOnlyMessage?: IMarkdownString; readonly onDidChangeCapabilities: Event; readonly onDidChangeFile: Event; @@ -688,6 +687,20 @@ export function hasFileAtomicDeleteCapability(provider: IFileSystemProvider): pr return !!(provider.capabilities & FileSystemProviderCapabilities.FileAtomicDelete); } +export interface IFileSystemProviderWithReadonlyCapability extends IFileSystemProvider { + + readonly capabilities: FileSystemProviderCapabilities.Readonly & FileSystemProviderCapabilities; + + /** + * An optional message to show in the UI to explain why the file system is readonly. + */ + readonly readOnlyMessage?: IMarkdownString; +} + +export function hasReadonlyCapability(provider: IFileSystemProvider): provider is IFileSystemProviderWithReadonlyCapability { + return !!(provider.capabilities & FileSystemProviderCapabilities.Readonly); +} + export enum FileSystemProviderErrorCode { FileExists = 'EntryExists', FileNotFound = 'EntryNotFound', diff --git a/src/vs/workbench/browser/contextkeys.ts b/src/vs/workbench/browser/contextkeys.ts index ae2e66a347a..09245ec08a3 100644 --- a/src/vs/workbench/browser/contextkeys.ts +++ b/src/vs/workbench/browser/contextkeys.ts @@ -307,7 +307,7 @@ export class WorkbenchContextKeysHandler extends Disposable { this.activeEditorAvailableEditorIds.set(editors.join(',')); } - this.activeEditorIsReadonly.set(activeEditorPane.input.hasCapability(EditorInputCapabilities.Readonly)); + this.activeEditorIsReadonly.set(!!activeEditorPane.input.isReadonly()); const primaryEditorResource = EditorResourceAccessor.getOriginalUri(activeEditorPane.input, { supportSideBySide: SideBySideEditor.PRIMARY }); this.activeEditorCanToggleReadonly.set(!!primaryEditorResource && this.fileService.hasProvider(primaryEditorResource) && !this.fileService.hasCapability(primaryEditorResource, FileSystemProviderCapabilities.Readonly)); } else { diff --git a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts index dc1669460a9..c16023cc81a 100644 --- a/src/vs/workbench/browser/parts/editor/editorAutoSave.ts +++ b/src/vs/workbench/browser/parts/editor/editorAutoSave.ts @@ -91,7 +91,7 @@ export class EditorAutoSave extends Disposable implements IWorkbenchContribution } private maybeTriggerAutoSave(reason: SaveReason, editorIdentifier?: IEditorIdentifier): void { - if (editorIdentifier?.editor.hasCapability(EditorInputCapabilities.Readonly) || editorIdentifier?.editor.hasCapability(EditorInputCapabilities.Untitled)) { + if (editorIdentifier?.editor.isReadonly() || editorIdentifier?.editor.hasCapability(EditorInputCapabilities.Untitled)) { return; // no auto save for readonly or untitled editors } diff --git a/src/vs/workbench/browser/parts/editor/editorStatus.ts b/src/vs/workbench/browser/parts/editor/editorStatus.ts index f7c809fa896..4938c99f3b9 100644 --- a/src/vs/workbench/browser/parts/editor/editorStatus.ts +++ b/src/vs/workbench/browser/parts/editor/editorStatus.ts @@ -13,7 +13,7 @@ import { URI } from 'vs/base/common/uri'; import { Action } from 'vs/base/common/actions'; import { Language } from 'vs/base/common/platform'; import { UntitledTextEditorInput } from 'vs/workbench/services/untitled/common/untitledTextEditorInput'; -import { IFileEditorInput, EditorResourceAccessor, IEditorPane, SideBySideEditor, EditorInputCapabilities } from 'vs/workbench/common/editor'; +import { IFileEditorInput, EditorResourceAccessor, IEditorPane, SideBySideEditor } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { Disposable, MutableDisposable, DisposableStore } from 'vs/base/common/lifecycle'; import { IEditorAction } from 'vs/editor/common/editorCommon'; @@ -340,7 +340,7 @@ export class EditorStatus extends Disposable implements IWorkbenchContribution { return this.quickInputService.pick([{ label: localize('noEditor', "No text editor active at this time") }]); } - if (this.editorService.activeEditor?.hasCapability(EditorInputCapabilities.Readonly)) { + if (this.editorService.activeEditor?.isReadonly()) { return this.quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); } @@ -1271,7 +1271,7 @@ export class ChangeEOLAction extends Action2 { return; } - if (editorService.activeEditor?.hasCapability(EditorInputCapabilities.Readonly)) { + if (editorService.activeEditor?.isReadonly()) { await quickInputService.pick([{ label: localize('noWritableCodeEditor', "The active code editor is read-only.") }]); return; } @@ -1288,7 +1288,7 @@ export class ChangeEOLAction extends Action2 { const eol = await quickInputService.pick(EOLOptions, { placeHolder: localize('pickEndOfLine', "Select End of Line Sequence"), activeItem: EOLOptions[selectedIndex] }); if (eol) { const activeCodeEditor = getCodeEditor(editorService.activeTextEditorControl); - if (activeCodeEditor?.hasModel() && !editorService.activeEditor?.hasCapability(EditorInputCapabilities.Readonly)) { + if (activeCodeEditor?.hasModel() && !editorService.activeEditor?.isReadonly()) { textModel = activeCodeEditor.getModel(); textModel.pushStackElement(); textModel.pushEOL(eol.eol); @@ -1353,7 +1353,7 @@ export class ChangeEncodingAction extends Action2 { let action: IQuickPickItem | undefined; if (encodingSupport instanceof UntitledTextEditorInput) { action = saveWithEncodingPick; - } else if (activeEditorPane.input.hasCapability(EditorInputCapabilities.Readonly)) { + } else if (activeEditorPane.input.isReadonly()) { action = reopenWithEncodingPick; } else { action = await quickInputService.pick([reopenWithEncodingPick, saveWithEncodingPick], { placeHolder: localize('pickAction', "Select Action"), matchOnDetail: true }); diff --git a/src/vs/workbench/browser/parts/editor/editorWithViewState.ts b/src/vs/workbench/browser/parts/editor/editorWithViewState.ts index ef20010c48a..f01bedd8f59 100644 --- a/src/vs/workbench/browser/parts/editor/editorWithViewState.ts +++ b/src/vs/workbench/browser/parts/editor/editorWithViewState.ts @@ -17,7 +17,6 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IExtUri } from 'vs/base/common/resources'; import { IDisposable, MutableDisposable } from 'vs/base/common/lifecycle'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; -import { IMarkdownString } from 'vs/base/common/htmlContent'; /** * Base class of editors that want to store and restore view state. @@ -54,12 +53,6 @@ export abstract class AbstractEditorWithViewState extends Edit super.setEditorVisible(visible, group); } - protected readonlyValues(isReadonly: boolean | IMarkdownString | undefined): { readOnly: boolean; readOnlyMessage: IMarkdownString | undefined } { - const readOnly = !!isReadonly; - const readOnlyMessage = typeof isReadonly !== 'boolean' ? isReadonly : undefined; - return { readOnly, readOnlyMessage }; - } - private onWillCloseEditor(e: IEditorCloseEvent): void { const editor = e.editor; if (editor === this.input) { diff --git a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts index 16447fd2fb8..19048f9ae60 100644 --- a/src/vs/workbench/browser/parts/editor/textDiffEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textDiffEditor.ts @@ -9,7 +9,7 @@ import { isObject, assertIsDefined, withNullAsUndefined } from 'vs/base/common/t import { ICodeEditor, IDiffEditor } from 'vs/editor/browser/editorBrowser'; import { IDiffEditorOptions, IEditorOptions as ICodeEditorOptions } from 'vs/editor/common/config/editorOptions'; import { AbstractTextEditor, IEditorConfiguration } from 'vs/workbench/browser/parts/editor/textEditor'; -import { TEXT_DIFF_EDITOR_ID, IEditorFactoryRegistry, EditorExtensions, ITextDiffEditorPane, IEditorOpenContext, EditorInputCapabilities, isEditorInput, isTextEditorViewState, createTooLargeFileError } from 'vs/workbench/common/editor'; +import { TEXT_DIFF_EDITOR_ID, IEditorFactoryRegistry, EditorExtensions, ITextDiffEditorPane, IEditorOpenContext, isEditorInput, isTextEditorViewState, createTooLargeFileError } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { applyTextEditorOptions } from 'vs/workbench/common/editor/editorOptions'; import { DiffEditorInput } from 'vs/workbench/common/editor/diffEditorInput'; @@ -160,7 +160,7 @@ export class TextDiffEditor extends AbstractTextEditor imp // a resolved model might have more specific information about being // readonly or not that the input did not have. control.updateOptions({ - ...this.readonlyValues(resolvedDiffEditorModel.modifiedModel?.isReadonly()), + ...this.getReadonlyConfiguration(resolvedDiffEditorModel.modifiedModel?.isReadonly()), originalEditable: !resolvedDiffEditorModel.originalModel?.isReadonly() }); @@ -280,12 +280,10 @@ export class TextDiffEditor extends AbstractTextEditor imp } protected override getConfigurationOverrides(): IDiffEditorOptions { - const readOnly = this.input instanceof DiffEditorInput && this.input.modified.hasCapability(EditorInputCapabilities.Readonly); - return { ...super.getConfigurationOverrides(), - readOnly, - originalEditable: this.input instanceof DiffEditorInput && !this.input.original.hasCapability(EditorInputCapabilities.Readonly), + ...this.getReadonlyConfiguration(this.input?.isReadonly()), + originalEditable: this.input instanceof DiffEditorInput && !this.input.original.isReadonly(), lineDecorationsWidth: '2ch' }; } @@ -293,8 +291,8 @@ export class TextDiffEditor extends AbstractTextEditor imp protected override updateReadonly(input: EditorInput): void { if (input instanceof DiffEditorInput) { this.diffEditorControl?.updateOptions({ - readOnly: input.hasCapability(EditorInputCapabilities.Readonly), - originalEditable: !input.original.hasCapability(EditorInputCapabilities.Readonly), + ...this.getReadonlyConfiguration(input.isReadonly()), + originalEditable: !input.original.isReadonly(), }); } else { super.updateReadonly(input); diff --git a/src/vs/workbench/browser/parts/editor/textEditor.ts b/src/vs/workbench/browser/parts/editor/textEditor.ts index af0fb470503..ca459e1d841 100644 --- a/src/vs/workbench/browser/parts/editor/textEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textEditor.ts @@ -10,7 +10,7 @@ import { Emitter, Event } from 'vs/base/common/event'; import { isObject, assertIsDefined } from 'vs/base/common/types'; import { MutableDisposable } from 'vs/base/common/lifecycle'; import { ICodeEditor } from 'vs/editor/browser/editorBrowser'; -import { IEditorOpenContext, EditorInputCapabilities, IEditorPaneSelection, EditorPaneSelectionCompareResult, EditorPaneSelectionChangeReason, IEditorPaneWithSelection, IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; +import { IEditorOpenContext, IEditorPaneSelection, EditorPaneSelectionCompareResult, EditorPaneSelectionChangeReason, IEditorPaneWithSelection, IEditorPaneSelectionChangeEvent } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { computeEditorAriaLabel } from 'vs/workbench/browser/editor'; import { AbstractEditorWithViewState } from 'vs/workbench/browser/parts/editor/editorWithViewState'; @@ -28,6 +28,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic import { IEditorOptions, ITextEditorOptions, TextEditorSelectionRevealType, TextEditorSelectionSource } from 'vs/platform/editor/common/editor'; import { ICursorPositionChangedEvent } from 'vs/editor/common/cursorEvents'; import { IFileService } from 'vs/platform/files/common/files'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; export interface IEditorConfiguration { editor: object; @@ -139,8 +140,14 @@ export abstract class AbstractTextEditor extends Abs } protected updateReadonly(input: EditorInput): void { - const readOnly = input.hasCapability(EditorInputCapabilities.Readonly); - this.updateEditorControlOptions({ readOnly }); + this.updateEditorControlOptions({ ...this.getReadonlyConfiguration(input.isReadonly()) }); + } + + protected getReadonlyConfiguration(isReadonly: boolean | IMarkdownString | undefined): { readOnly: boolean; readOnlyMessage: IMarkdownString | undefined } { + return { + readOnly: !!isReadonly, + readOnlyMessage: typeof isReadonly !== 'boolean' ? isReadonly : undefined + }; } protected getConfigurationOverrides(): ICodeEditorOptions { @@ -148,7 +155,7 @@ export abstract class AbstractTextEditor extends Abs overviewRulerLanes: 3, lineNumbersMinChars: 3, fixedOverflowWidgets: true, - ...this.readonlyValues(this.input?.isReadonly()), + ...this.getReadonlyConfiguration(this.input?.isReadonly()), renderValidationDecorations: 'on' // render problems even in readonly editors (https://github.com/microsoft/vscode/issues/89057) }; } diff --git a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts index 899287c5ae3..6c88cf45b5f 100644 --- a/src/vs/workbench/browser/parts/editor/textResourceEditor.ts +++ b/src/vs/workbench/browser/parts/editor/textResourceEditor.ts @@ -92,7 +92,7 @@ export abstract class AbstractTextResourceEditor extends AbstractTextCodeEditor< // was already asked for being readonly or not. The rationale is that // a resolved model might have more specific information about being // readonly or not that the input did not have. - control.updateOptions(this.readonlyValues(resolvedModel.isReadonly())); + control.updateOptions(this.getReadonlyConfiguration(resolvedModel.isReadonly())); } /** diff --git a/src/vs/workbench/common/editor/editorInput.ts b/src/vs/workbench/common/editor/editorInput.ts index 739c6ab9b53..9ad242aba09 100644 --- a/src/vs/workbench/common/editor/editorInput.ts +++ b/src/vs/workbench/common/editor/editorInput.ts @@ -125,7 +125,7 @@ export abstract class EditorInput extends AbstractEditorInput { return (this.capabilities & capability) !== 0; } - isReadonly(): boolean | IMarkdownString | undefined { + isReadonly(): boolean | IMarkdownString { return this.hasCapability(EditorInputCapabilities.Readonly); } diff --git a/src/vs/workbench/common/editor/resourceEditorInput.ts b/src/vs/workbench/common/editor/resourceEditorInput.ts index 2a4c24f8939..d1d5f8c5674 100644 --- a/src/vs/workbench/common/editor/resourceEditorInput.ts +++ b/src/vs/workbench/common/editor/resourceEditorInput.ts @@ -10,6 +10,7 @@ import { IFileService } from 'vs/platform/files/common/files'; import { ILabelService } from 'vs/platform/label/common/label'; import { dirname, isEqual } from 'vs/base/common/resources'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; /** * The base class for all editor inputs that open resources. @@ -174,4 +175,8 @@ export abstract class AbstractResourceEditorInput extends EditorInput implements return this.mediumTitle; } } + + override isReadonly(): boolean | IMarkdownString { + return this.filesConfigurationService.isReadonly(this.resource); + } } diff --git a/src/vs/workbench/common/editor/sideBySideEditorInput.ts b/src/vs/workbench/common/editor/sideBySideEditorInput.ts index d36c91348a4..beee39963a2 100644 --- a/src/vs/workbench/common/editor/sideBySideEditorInput.ts +++ b/src/vs/workbench/common/editor/sideBySideEditorInput.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Event } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { URI } from 'vs/base/common/uri'; import { localize } from 'vs/nls'; import { IInstantiationService } from 'vs/platform/instantiation/common/instantiation'; @@ -246,6 +247,10 @@ export class SideBySideEditorInput extends EditorInput implements ISideBySideEdi return undefined; } + override isReadonly(): boolean | IMarkdownString { + return this.primary.isReadonly(); + } + override toUntyped(options?: { preserveViewState: GroupIdentifier }): IResourceSideBySideEditorInput | undefined { const primaryResourceEditorInput = this.primary.toUntyped(options); const secondaryResourceEditorInput = this.secondary.toUntyped(options); diff --git a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts index 61f4ab3a6ce..964a6a0443b 100644 --- a/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts +++ b/src/vs/workbench/contrib/customEditor/browser/customEditorInput.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { VSBuffer } from 'vs/base/common/buffer'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IReference } from 'vs/base/common/lifecycle'; import { Schemas } from 'vs/base/common/network'; import { basename } from 'vs/base/common/path'; @@ -251,6 +252,13 @@ export class CustomEditorInput extends LazilyResolvedWebviewEditorInput { return CustomEditorInput.create(this.instantiationService, this.resource, this.viewType, this.group, this.webview.options); } + public override isReadonly(): boolean | IMarkdownString { + if (!this._modelRef) { + return this.filesConfigurationService.isReadonly(this.resource); + } + return this._modelRef.object.isReadonly(); + } + public override isDirty(): boolean { if (!this._modelRef) { return !!this._defaultDirtyState; diff --git a/src/vs/workbench/contrib/customEditor/common/customEditor.ts b/src/vs/workbench/contrib/customEditor/common/customEditor.ts index aab705ab761..40c47ab8a80 100644 --- a/src/vs/workbench/contrib/customEditor/common/customEditor.ts +++ b/src/vs/workbench/contrib/customEditor/common/customEditor.ts @@ -5,6 +5,7 @@ import { distinct } from 'vs/base/common/arrays'; import { Event } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { IDisposable, IReference } from 'vs/base/common/lifecycle'; import { URI } from 'vs/base/common/uri'; import * as nls from 'vs/nls'; @@ -57,7 +58,7 @@ export interface ICustomEditorModel extends IDisposable { readonly resource: URI; readonly backupId: string | undefined; - isReadonly(): boolean; + isReadonly(): boolean | IMarkdownString; readonly onDidChangeReadonly: Event; isOrphaned(): boolean; diff --git a/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts index 36faa2dee55..cb0defd952f 100644 --- a/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts +++ b/src/vs/workbench/contrib/customEditor/common/customTextEditorModel.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { Emitter, Event } from 'vs/base/common/event'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; import { Disposable, IReference } from 'vs/base/common/lifecycle'; import { isEqual } from 'vs/base/common/resources'; import { URI } from 'vs/base/common/uri'; @@ -63,8 +64,8 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo return this._resource; } - public isReadonly(): boolean { - return !!this._model.object.isReadonly(); + public isReadonly(): boolean | IMarkdownString { + return this._model.object.isReadonly(); } public get backupId() { diff --git a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts index 6056a86eb64..faf84eafbf9 100644 --- a/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts +++ b/src/vs/workbench/contrib/files/browser/editors/fileEditorInput.ts @@ -193,8 +193,8 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements return this.preferredName; } - override isReadonly(): boolean | IMarkdownString | undefined { - return this.model ? this.model.isReadonly() : super.isReadonly(); + override isReadonly(): boolean | IMarkdownString { + return this.model ? this.model.isReadonly() : this.filesConfigurationService.isReadonly(this.resource); } override getDescription(verbosity?: Verbosity): string | undefined { diff --git a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts index cc17c775855..438accb72ad 100644 --- a/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts +++ b/src/vs/workbench/contrib/files/browser/editors/textFileEditor.ts @@ -147,7 +147,7 @@ export class TextFileEditor extends AbstractTextCodeEditor // was already asked for being readonly or not. The rationale is that // a resolved model might have more specific information about being // readonly or not that the input did not have. - control.updateOptions(this.readonlyValues(textFileModel.isReadonly())); + control.updateOptions(this.getReadonlyConfiguration(textFileModel.isReadonly())); } catch (error) { await this.handleSetInputError(error, input, options); } diff --git a/src/vs/workbench/contrib/files/browser/views/explorerView.ts b/src/vs/workbench/contrib/files/browser/views/explorerView.ts index 671c0bbd7f2..4572956371b 100644 --- a/src/vs/workbench/contrib/files/browser/views/explorerView.ts +++ b/src/vs/workbench/contrib/files/browser/views/explorerView.ts @@ -546,7 +546,7 @@ export class ExplorerView extends ViewPane implements IExplorerView { stat = stat || this.explorerService.findClosest(resource); this.resourceContext.set(resource); this.folderContext.set(!!stat && stat.isDirectory); - this.readonlyContext.set(!!stat && stat.isReadonly); + this.readonlyContext.set(!!stat && !!stat.isReadonly); this.rootContext.set(!!stat && stat.isRoot); if (resource) { diff --git a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts index 9a8c27ce3ad..554ee07c4f3 100644 --- a/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts +++ b/src/vs/workbench/contrib/files/browser/views/openEditorsView.ts @@ -13,7 +13,7 @@ import { IInstantiationService, ServicesAccessor } from 'vs/platform/instantiati import { IEditorGroupsService, IEditorGroup, GroupsOrder, GroupOrientation } from 'vs/workbench/services/editor/common/editorGroupsService'; import { IConfigurationService, IConfigurationChangeEvent } from 'vs/platform/configuration/common/configuration'; import { IKeybindingService } from 'vs/platform/keybinding/common/keybinding'; -import { Verbosity, EditorResourceAccessor, SideBySideEditor, EditorInputCapabilities, IEditorIdentifier, GroupModelChangeKind } from 'vs/workbench/common/editor'; +import { Verbosity, EditorResourceAccessor, SideBySideEditor, IEditorIdentifier, GroupModelChangeKind } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SaveAllInGroupAction, CloseGroupAction } from 'vs/workbench/contrib/files/browser/fileActions'; import { OpenEditorsFocusedContext, ExplorerFocusedContext, IFilesConfiguration, OpenEditor } from 'vs/workbench/contrib/files/common/files'; @@ -258,7 +258,7 @@ export class OpenEditorsView extends ViewPane { if (element instanceof OpenEditor) { const resource = element.getResource(); this.dirtyEditorFocusedContext.set(element.editor.isDirty() && !element.editor.isSaving()); - this.readonlyEditorFocusedContext.set(element.editor.hasCapability(EditorInputCapabilities.Readonly)); + this.readonlyEditorFocusedContext.set(!!element.editor.isReadonly()); this.resourceContext.set(withUndefinedAsNull(resource)); } else if (!!element) { this.groupFocusedContext.set(true); diff --git a/src/vs/workbench/contrib/files/common/explorerModel.ts b/src/vs/workbench/contrib/files/common/explorerModel.ts index 30117c116d8..513b29151ef 100644 --- a/src/vs/workbench/contrib/files/common/explorerModel.ts +++ b/src/vs/workbench/contrib/files/common/explorerModel.ts @@ -21,6 +21,7 @@ import { ExplorerFileNestingTrie } from 'vs/workbench/contrib/files/common/explo import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; import { assertIsDefined } from 'vs/base/common/types'; import { IFilesConfigurationService } from 'vs/workbench/services/filesConfiguration/common/filesConfigurationService'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; export class ExplorerModel implements IDisposable { @@ -148,8 +149,8 @@ export class ExplorerItem { return !!this._isDirectory; } - get isReadonly(): boolean { - return !!this.filesConfigService.isReadonly(this.resource, { resource: this.resource, name: this.name, readonly: this._readonly, locked: this._locked }); + get isReadonly(): boolean | IMarkdownString { + return this.filesConfigService.isReadonly(this.resource, { resource: this.resource, name: this.name, readonly: this._readonly, locked: this._locked }); } get mtime(): number | undefined { diff --git a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts index cc86fb6f8cd..57ac5ff9356 100644 --- a/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts +++ b/src/vs/workbench/contrib/files/test/browser/fileEditorInput.test.ts @@ -70,6 +70,7 @@ suite('Files - FileEditorInput', () => { assert.ok(!input.hasCapability(EditorInputCapabilities.Untitled)); assert.ok(!input.hasCapability(EditorInputCapabilities.Readonly)); + assert.ok(!input.isReadonly()); assert.ok(!input.hasCapability(EditorInputCapabilities.Singleton)); assert.ok(!input.hasCapability(EditorInputCapabilities.RequiresTrust)); @@ -123,6 +124,7 @@ suite('Files - FileEditorInput', () => { assert.ok(input.hasCapability(EditorInputCapabilities.Untitled)); assert.ok(!input.hasCapability(EditorInputCapabilities.Readonly)); + assert.ok(!input.isReadonly()); }); test('reports as readonly with readonly file scheme', async function () { @@ -136,6 +138,7 @@ suite('Files - FileEditorInput', () => { assert.ok(!input.hasCapability(EditorInputCapabilities.Untitled)); assert.ok(input.hasCapability(EditorInputCapabilities.Readonly)); + assert.ok(input.isReadonly()); } finally { disposable.dispose(); } @@ -431,6 +434,7 @@ suite('Files - FileEditorInput', () => { assert.strictEqual(model.isReadonly(), false); assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), false); + assert.strictEqual(input.isReadonly(), false); const stat = await accessor.fileService.resolve(input.resource, { resolveMetadata: true }); @@ -441,8 +445,9 @@ suite('Files - FileEditorInput', () => { accessor.fileService.readShouldThrowError = undefined; } - assert.strictEqual(model.isReadonly(), true); + assert.strictEqual(!!model.isReadonly(), true); assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), true); + assert.strictEqual(!!input.isReadonly(), true); assert.strictEqual(listenerCount, 1); try { @@ -454,6 +459,7 @@ suite('Files - FileEditorInput', () => { assert.strictEqual(model.isReadonly(), false); assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), false); + assert.strictEqual(input.isReadonly(), false); assert.strictEqual(listenerCount, 2); input.dispose(); diff --git a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts index d00065a31da..af68d86e8a7 100644 --- a/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts +++ b/src/vs/workbench/contrib/notebook/browser/notebookEditor.ts @@ -24,7 +24,7 @@ import { ITelemetryService } from 'vs/platform/telemetry/common/telemetry'; import { IThemeService } from 'vs/platform/theme/common/themeService'; import { Selection } from 'vs/editor/common/core/selection'; import { EditorPane } from 'vs/workbench/browser/parts/editor/editorPane'; -import { DEFAULT_EDITOR_ASSOCIATION, EditorInputCapabilities, EditorPaneSelectionChangeReason, EditorPaneSelectionCompareResult, EditorResourceAccessor, IEditorMemento, IEditorOpenContext, IEditorPaneSelection, IEditorPaneSelectionChangeEvent, createEditorOpenError, isEditorOpenError } from 'vs/workbench/common/editor'; +import { DEFAULT_EDITOR_ASSOCIATION, EditorPaneSelectionChangeReason, EditorPaneSelectionCompareResult, EditorResourceAccessor, IEditorMemento, IEditorOpenContext, IEditorPaneSelection, IEditorPaneSelectionChangeEvent, createEditorOpenError, isEditorOpenError } from 'vs/workbench/common/editor'; import { EditorInput } from 'vs/workbench/common/editor/editorInput'; import { SELECT_KERNEL_ID } from 'vs/workbench/contrib/notebook/browser/controller/coreActions'; import { INotebookEditorOptions, INotebookEditorPane, INotebookEditorViewState } from 'vs/workbench/contrib/notebook/browser/notebookBrowser'; @@ -111,7 +111,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { } private _updateReadonly(input: NotebookEditorInput): void { - this._widget.value?.setOptions({ isReadOnly: input.hasCapability(EditorInputCapabilities.Readonly) }); + this._widget.value?.setOptions({ isReadOnly: !!input.isReadonly() }); } get textModel(): NotebookTextModel | undefined { @@ -301,7 +301,7 @@ export class NotebookEditor extends EditorPane implements INotebookEditorPane { this._widget.value!.setEditorProgressService(this._editorProgressService); await this._widget.value!.setModel(model.notebook, viewState, perf); - const isReadOnly = input.hasCapability(EditorInputCapabilities.Readonly); + const isReadOnly = !!input.isReadonly(); await this._widget.value!.setOptions({ ...options, isReadOnly }); this._widgetDisposableStore.add(this._widget.value!.onDidFocusWidget(() => this._onDidFocusWidget.fire())); this._widgetDisposableStore.add(this._widget.value!.onDidBlurWidget(() => this._onDidBlurWidget.fire())); diff --git a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts index 7a21a37927a..4b9186cb43f 100644 --- a/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts +++ b/src/vs/workbench/contrib/notebook/common/notebookEditorInput.ts @@ -28,6 +28,7 @@ import { IFilesConfigurationService } from 'vs/workbench/services/filesConfigura import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions'; import { localize } from 'vs/nls'; import { IEditorService } from 'vs/workbench/services/editor/common/editorService'; +import { IMarkdownString } from 'vs/base/common/htmlContent'; export interface NotebookEditorInputOptions { startDirty?: boolean; @@ -141,6 +142,13 @@ export class NotebookEditorInput extends AbstractResourceEditorInput { return undefined; // no description for untitled notebooks without associated file path } + override isReadonly(): boolean | IMarkdownString { + if (!this._editorModelReference) { + return this.filesConfigurationService.isReadonly(this.resource); + } + return this._editorModelReference.object.isReadonly(); + } + override isDirty() { if (!this._editorModelReference) { return this._defaultDirtyState; diff --git a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts index fb8ee256c5c..19d723d383a 100644 --- a/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts +++ b/src/vs/workbench/services/filesConfiguration/common/filesConfigurationService.ts @@ -3,13 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { localize } from 'vs/nls'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; import { InstantiationType, registerSingleton } from 'vs/platform/instantiation/common/extensions'; import { Event, Emitter } from 'vs/base/common/event'; import { Disposable } from 'vs/base/common/lifecycle'; import { RawContextKey, IContextKey, IContextKeyService } from 'vs/platform/contextkey/common/contextkey'; import { IConfigurationService } from 'vs/platform/configuration/common/configuration'; -import { IFilesConfiguration, AutoSaveConfiguration, HotExitConfiguration, FILES_READONLY_INCLUDE_CONFIG, FILES_READONLY_EXCLUDE_CONFIG, IFileStatWithMetadata, IFileService, FileSystemProviderCapabilities, IBaseFileStat } from 'vs/platform/files/common/files'; +import { IFilesConfiguration, AutoSaveConfiguration, HotExitConfiguration, FILES_READONLY_INCLUDE_CONFIG, FILES_READONLY_EXCLUDE_CONFIG, IFileStatWithMetadata, IFileService, IBaseFileStat, hasReadonlyCapability } from 'vs/platform/files/common/files'; import { equals } from 'vs/base/common/objects'; import { URI } from 'vs/base/common/uri'; import { isWeb } from 'vs/base/common/platform'; @@ -25,9 +26,9 @@ import { IMarkdownString } from 'vs/base/common/htmlContent'; export const AutoSaveAfterShortDelayContext = new RawContextKey('autoSaveAfterShortDelayContext', false, true); export interface IAutoSaveConfiguration { - autoSaveDelay?: number; - autoSaveFocusChange: boolean; - autoSaveApplicationChange: boolean; + readonly autoSaveDelay?: number; + readonly autoSaveFocusChange: boolean; + readonly autoSaveApplicationChange: boolean; } export const enum AutoSaveMode { @@ -79,7 +80,15 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi declare readonly _serviceBrand: undefined; - private static DEFAULT_AUTO_SAVE_MODE = isWeb ? AutoSaveConfiguration.AFTER_DELAY : AutoSaveConfiguration.OFF; + private static readonly DEFAULT_AUTO_SAVE_MODE = isWeb ? AutoSaveConfiguration.AFTER_DELAY : AutoSaveConfiguration.OFF; + + private static readonly READONLY_MESSAGES = { + providerReadonly: { value: localize('providerReadonly', "Editor is read-only because the file system of the file is read-only."), isTrusted: true }, + sessionReadonly: { value: localize({ key: 'sessionReadonly', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because the file was set read-only in this session. [Click here](command:{0}) to set writeable.", 'workbench.action.files.setActiveEditorWriteableInSession'), isTrusted: true }, + configuredReadonly: { value: localize({ key: 'configuredReadonly', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because the file was set read-only via settings. [Click here](command:{0}) to configure.", `workbench.action.openSettings?${encodeURIComponent('["files.readonly"]')}`), isTrusted: true }, + fileLocked: { value: localize({ key: 'fileLocked', comment: ['Please do not translate the word "command", it is part of our internal syntax which must not change', '{Locked="](command:{0})"}'] }, "Editor is read-only because of file permissions. [Click here](command:{0}) to set writeable anyway.", 'workbench.action.files.setActiveEditorWriteableInSession'), isTrusted: true }, + fileReadonly: { value: localize('fileReadonly', "Editor is read-only because the file is read-only."), isTrusted: true } + }; private readonly _onAutoSaveConfigurationChange = this._register(new Emitter()); readonly onAutoSaveConfigurationChange = this._onAutoSaveConfigurationChange.event; @@ -104,7 +113,7 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi private readonly readonlyExcludeMatcher = this._register(new IdleValue(() => this.createReadonlyMatcher(FILES_READONLY_EXCLUDE_CONFIG))); private configuredReadonlyFromPermissions: boolean | undefined; - private readonly sessionReadonlyOverrides = new ResourceMap(resource => this.uriIdentityService.extUri.getComparisonKey(resource)); + private readonly sessionReadonlyOverrides = new ResourceMap(resource => this.uriIdentityService.extUri.getComparisonKey(resource)); constructor( @IContextKeyService contextKeyService: IContextKeyService, @@ -146,14 +155,15 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi // if the entire file system provider is readonly, we respect that // and do not allow to change readonly. we take this as a hint that // the provider has no capabilities of writing. - if (this.fileService.hasCapability(resource, FileSystemProviderCapabilities.Readonly)) { - return this.fileService.getProvider(resource.scheme)?.readOnlyMessage ?? true; + const provider = this.fileService.getProvider(resource.scheme); + if (provider && hasReadonlyCapability(provider)) { + return provider.readOnlyMessage ?? FilesConfigurationService.READONLY_MESSAGES.providerReadonly; } // session override always wins over the others const sessionReadonlyOverride = this.sessionReadonlyOverrides.get(resource); if (typeof sessionReadonlyOverride === 'boolean') { - return sessionReadonlyOverride; + return sessionReadonlyOverride === true ? FilesConfigurationService.READONLY_MESSAGES.sessionReadonly : false; } if ( @@ -165,11 +175,20 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi // configured glob patterns win over stat information if (this.readonlyIncludeMatcher.value.matches(resource)) { - return !this.readonlyExcludeMatcher.value.matches(resource); + return !this.readonlyExcludeMatcher.value.matches(resource) ? FilesConfigurationService.READONLY_MESSAGES.configuredReadonly : false; } - // finally check for stat information - return (this.configuredReadonlyFromPermissions && stat?.locked) ?? stat?.readonly ?? false; + // check if file is locked and configured to treat as readonly + if (this.configuredReadonlyFromPermissions && stat?.locked) { + return FilesConfigurationService.READONLY_MESSAGES.fileLocked; + } + + // check if file is marked readonly from the file system provider + if (stat?.readonly) { + return FilesConfigurationService.READONLY_MESSAGES.fileReadonly; + } + + return false; } async updateReadonly(resource: URI, readonly: true | false | 'toggle' | 'reset'): Promise { diff --git a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts index b1e089dcc1c..c724a93fd1b 100644 --- a/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts +++ b/src/vs/workbench/services/untitled/test/browser/untitledTextEditor.test.ts @@ -53,6 +53,7 @@ suite('Untitled text editors', () => { assert.ok(input1.hasCapability(EditorInputCapabilities.Untitled)); assert.ok(!input1.hasCapability(EditorInputCapabilities.Readonly)); + assert.ok(!input1.isReadonly()); assert.ok(!input1.hasCapability(EditorInputCapabilities.Singleton)); assert.ok(!input1.hasCapability(EditorInputCapabilities.RequiresTrust)); assert.ok(!input1.hasCapability(EditorInputCapabilities.Scratchpad)); 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 12db4ee949a..69eb8467548 100644 --- a/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts +++ b/src/vs/workbench/services/workingCopy/test/browser/storedFileWorkingCopy.test.ts @@ -390,7 +390,7 @@ suite('StoredFileWorkingCopy', function () { accessor.fileService.readShouldThrowError = undefined; } - assert.strictEqual(workingCopy.isReadonly(), true); + assert.strictEqual(!!workingCopy.isReadonly(), true); assert.strictEqual(readonlyChangeCounter, 1); try { @@ -914,7 +914,7 @@ suite('StoredFileWorkingCopy', function () { await workingCopy.resolve(); - assert.strictEqual(workingCopy.isReadonly(), true); + assert.strictEqual(!!workingCopy.isReadonly(), true); accessor.fileService.readonly = false; diff --git a/src/vs/workbench/test/browser/parts/editor/editor.test.ts b/src/vs/workbench/test/browser/parts/editor/editor.test.ts index 3ecf10644ed..a49e5dd00c1 100644 --- a/src/vs/workbench/test/browser/parts/editor/editor.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/editor.test.ts @@ -105,12 +105,14 @@ suite('Workbench editor utils', () => { testInput1.capabilities = EditorInputCapabilities.None; assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.None), true); assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.Readonly), false); + assert.strictEqual(testInput1.isReadonly(), false); assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.Untitled), false); assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.RequiresTrust), false); assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.Singleton), false); testInput1.capabilities |= EditorInputCapabilities.Readonly; assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.Readonly), true); + assert.strictEqual(!!testInput1.isReadonly(), true); assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.None), false); assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.Untitled), false); assert.strictEqual(testInput1.hasCapability(EditorInputCapabilities.RequiresTrust), false); @@ -122,15 +124,18 @@ suite('Workbench editor utils', () => { const sideBySideInput = instantiationService.createInstance(SideBySideEditorInput, 'name', undefined, testInput1, testInput2); assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.MultipleEditors), true); assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Readonly), false); + assert.strictEqual(sideBySideInput.isReadonly(), false); assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Untitled), false); assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.RequiresTrust), false); assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Singleton), false); testInput1.capabilities |= EditorInputCapabilities.Readonly; assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Readonly), false); + assert.strictEqual(sideBySideInput.isReadonly(), false); testInput2.capabilities |= EditorInputCapabilities.Readonly; assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Readonly), true); + assert.strictEqual(!!sideBySideInput.isReadonly(), true); testInput1.capabilities |= EditorInputCapabilities.Untitled; assert.strictEqual(sideBySideInput.hasCapability(EditorInputCapabilities.Untitled), false); diff --git a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts index 12ed533e881..2fd994d4639 100644 --- a/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts +++ b/src/vs/workbench/test/browser/parts/editor/resourceEditorInput.test.ts @@ -58,6 +58,7 @@ suite('ResourceEditorInput', () => { assert.ok(input.getTitle(Verbosity.LONG).length > 0); assert.strictEqual(input.hasCapability(EditorInputCapabilities.Readonly), false); + assert.strictEqual(input.isReadonly(), false); assert.strictEqual(input.hasCapability(EditorInputCapabilities.Untitled), true); }); });