readonly - adopt readOnlyMessage for readonly mode (#185756)

This commit is contained in:
Benjamin Pasero
2023-06-21 14:40:17 +02:00
committed by GitHub
parent f596237ae6
commit 7190530d35
28 changed files with 134 additions and 58 deletions
+4
View File
@@ -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"
+14 -1
View File
@@ -584,7 +584,6 @@ export const enum FileSystemProviderCapabilities {
export interface IFileSystemProvider {
readonly capabilities: FileSystemProviderCapabilities;
readonly readOnlyMessage?: IMarkdownString;
readonly onDidChangeCapabilities: Event<void>;
readonly onDidChangeFile: Event<readonly IFileChange[]>;
@@ -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',
+1 -1
View File
@@ -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 {
@@ -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
}
@@ -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 });
@@ -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<T extends object> 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) {
@@ -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<IDiffEditorViewState> 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<IDiffEditorViewState> 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<IDiffEditorViewState> 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);
@@ -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<T extends IEditorViewState> 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<T extends IEditorViewState> 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)
};
}
@@ -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()));
}
/**
@@ -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);
}
@@ -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);
}
}
@@ -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);
@@ -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;
@@ -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<void>;
isOrphaned(): boolean;
@@ -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() {
@@ -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 {
@@ -147,7 +147,7 @@ export class TextFileEditor extends AbstractTextCodeEditor<ICodeEditorViewState>
// 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);
}
@@ -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) {
@@ -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);
@@ -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 {
@@ -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();
@@ -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()));
@@ -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;
@@ -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<boolean>('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<IAutoSaveConfiguration>());
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<boolean | IMarkdownString>(resource => this.uriIdentityService.extUri.getComparisonKey(resource));
private readonly sessionReadonlyOverrides = new ResourceMap<boolean>(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<void> {
@@ -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));
@@ -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;
@@ -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);
@@ -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);
});
});