Set a read-only message from an extension (#185216)

* wip

* Allow extensions to provide a readonly message
Part of #166971

* Address feedback

* Further address feedback

* Fix some nits

* Add test

* Improve tests and respond to feedback

* Don't render editor.readOnlyMessage in the settings UI

* No need to validate the IMarkdownString

---------

Co-authored-by: Benjamin Pasero <benjamin.pasero@microsoft.com>
Co-authored-by: Alex Dima <alexdima@microsoft.com>
This commit is contained in:
Alex Ross 2023-06-21 10:55:00 +02:00 committed by GitHub
parent 76cc1fc7cc
commit 1a4e466fc0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 349 additions and 151 deletions

View file

@ -26,6 +26,7 @@
"notebookMime",
"portsAttributes",
"quickPickSortByLabel",
"readonlyMessage",
"resolvers",
"saveEditor",
"scmActionButton",

View file

@ -0,0 +1,63 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import * as assert from 'assert';
import * as vscode from 'vscode';
import { TestFS } from '../memfs';
import { assertNoRpc, closeAllEditors } from '../utils';
suite('vscode API - file system', () => {
teardown(async function () {
assertNoRpc();
await closeAllEditors();
});
test('readonly file system - boolean', async function () {
const fs = new TestFS('this-fs', false);
const reg = vscode.workspace.registerFileSystemProvider(fs.scheme, fs, { isReadonly: true });
let error: any | undefined;
try {
await vscode.workspace.fs.writeFile(vscode.Uri.parse('this-fs:/foo.txt'), Buffer.from('Hello World'));
} catch (e) {
error = e;
}
assert.strictEqual(vscode.workspace.fs.isWritableFileSystem('this-fs'), false);
assert.strictEqual(error instanceof vscode.FileSystemError, true);
const fileError: vscode.FileSystemError = error;
assert.strictEqual(fileError.code, 'NoPermissions');
reg.dispose();
});
test('readonly file system - markdown', async function () {
const fs = new TestFS('this-fs', false);
const reg = vscode.workspace.registerFileSystemProvider(fs.scheme, fs, { isReadonly: new vscode.MarkdownString('This file is readonly.') });
let error: any | undefined;
try {
await vscode.workspace.fs.writeFile(vscode.Uri.parse('this-fs:/foo.txt'), Buffer.from('Hello World'));
} catch (e) {
error = e;
}
assert.strictEqual(vscode.workspace.fs.isWritableFileSystem('this-fs'), false);
assert.strictEqual(error instanceof vscode.FileSystemError, true);
const fileError: vscode.FileSystemError = error;
assert.strictEqual(fileError.code, 'NoPermissions');
reg.dispose();
});
test('writeable file system', async function () {
const fs = new TestFS('this-fs', false);
const reg = vscode.workspace.registerFileSystemProvider(fs.scheme, fs);
let error: any | undefined;
try {
await vscode.workspace.fs.writeFile(vscode.Uri.parse('this-fs:/foo.txt'), Buffer.from('Hello World'));
} catch (e) {
error = e;
}
assert.strictEqual(vscode.workspace.fs.isWritableFileSystem('this-fs'), true);
assert.strictEqual(error, undefined);
reg.dispose();
});
});

View file

@ -16,6 +16,7 @@ import * as arrays from 'vs/base/common/arrays';
import * as objects from 'vs/base/common/objects';
import { EDITOR_MODEL_DEFAULTS } from 'vs/editor/common/core/textModelDefaults';
import { IDocumentDiffProvider } from 'vs/editor/common/diff/documentDiffProvider';
import { IMarkdownString } from 'vs/base/common/htmlContent';
//#region typed options
@ -151,6 +152,10 @@ export interface IEditorOptions {
* Defaults to false.
*/
readOnly?: boolean;
/**
* The message to display when the editor is readonly.
*/
readOnlyMessage?: IMarkdownString;
/**
* Should the textarea used for input use the DOM `readonly` attribute.
* Defaults to false.
@ -3459,6 +3464,30 @@ class EditorRulers extends BaseEditorOption<EditorOption.rulers, (number | IRule
//#endregion
//#region readonly
/**
* Configuration options for readonly message
*/
class ReadonlyMessage extends BaseEditorOption<EditorOption.readOnlyMessage, IMarkdownString | undefined, IMarkdownString | undefined> {
constructor() {
const defaults = undefined;
super(
EditorOption.readOnlyMessage, 'readOnlyMessage', defaults
);
}
public validate(_input: any): IMarkdownString | undefined {
if (!_input || typeof _input !== 'object') {
return this.defaultValue;
}
return _input as IMarkdownString;
}
}
//#endregion
//#region scrollbar
/**
@ -5024,6 +5053,7 @@ export const enum EditorOption {
quickSuggestions,
quickSuggestionsDelay,
readOnly,
readOnlyMessage,
renameOnType,
renderControlCharacters,
renderFinalNewline,
@ -5530,6 +5560,7 @@ export const EditorOptions = {
readOnly: register(new EditorBooleanOption(
EditorOption.readOnly, 'readOnly', false,
)),
readOnlyMessage: register(new ReadonlyMessage()),
renameOnType: register(new EditorBooleanOption(
EditorOption.renameOnType, 'renameOnType', false,
{ description: nls.localize('renameOnType', "Controls whether the editor auto renames on type."), markdownDeprecationMessage: nls.localize('renameOnTypeDeprecate', "Deprecated, use `editor.linkedEditing` instead.") }

View file

@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { IDisposable, IReference } from 'vs/base/common/lifecycle';
import { URI } from 'vs/base/common/uri';
import { ITextModel, ITextSnapshot } from 'vs/editor/common/model';
@ -55,7 +56,7 @@ export interface ITextEditorModel extends IEditorModel {
/**
* Signals if this model is readonly or not.
*/
isReadonly(): boolean;
isReadonly(): boolean | IMarkdownString;
/**
* The language id of the text model if known.

View file

@ -262,61 +262,62 @@ export enum EditorOption {
quickSuggestions = 86,
quickSuggestionsDelay = 87,
readOnly = 88,
renameOnType = 89,
renderControlCharacters = 90,
renderFinalNewline = 91,
renderLineHighlight = 92,
renderLineHighlightOnlyWhenFocus = 93,
renderValidationDecorations = 94,
renderWhitespace = 95,
revealHorizontalRightPadding = 96,
roundedSelection = 97,
rulers = 98,
scrollbar = 99,
scrollBeyondLastColumn = 100,
scrollBeyondLastLine = 101,
scrollPredominantAxis = 102,
selectionClipboard = 103,
selectionHighlight = 104,
selectOnLineNumbers = 105,
showFoldingControls = 106,
showUnused = 107,
snippetSuggestions = 108,
smartSelect = 109,
smoothScrolling = 110,
stickyScroll = 111,
stickyTabStops = 112,
stopRenderingLineAfter = 113,
suggest = 114,
suggestFontSize = 115,
suggestLineHeight = 116,
suggestOnTriggerCharacters = 117,
suggestSelection = 118,
tabCompletion = 119,
tabIndex = 120,
unicodeHighlighting = 121,
unusualLineTerminators = 122,
useShadowDOM = 123,
useTabStops = 124,
wordBreak = 125,
wordSeparators = 126,
wordWrap = 127,
wordWrapBreakAfterCharacters = 128,
wordWrapBreakBeforeCharacters = 129,
wordWrapColumn = 130,
wordWrapOverride1 = 131,
wordWrapOverride2 = 132,
wrappingIndent = 133,
wrappingStrategy = 134,
showDeprecated = 135,
inlayHints = 136,
editorClassName = 137,
pixelRatio = 138,
tabFocusMode = 139,
layoutInfo = 140,
wrappingInfo = 141,
defaultColorDecorators = 142,
colorDecoratorsActivatedOn = 143
readOnlyMessage = 89,
renameOnType = 90,
renderControlCharacters = 91,
renderFinalNewline = 92,
renderLineHighlight = 93,
renderLineHighlightOnlyWhenFocus = 94,
renderValidationDecorations = 95,
renderWhitespace = 96,
revealHorizontalRightPadding = 97,
roundedSelection = 98,
rulers = 99,
scrollbar = 100,
scrollBeyondLastColumn = 101,
scrollBeyondLastLine = 102,
scrollPredominantAxis = 103,
selectionClipboard = 104,
selectionHighlight = 105,
selectOnLineNumbers = 106,
showFoldingControls = 107,
showUnused = 108,
snippetSuggestions = 109,
smartSelect = 110,
smoothScrolling = 111,
stickyScroll = 112,
stickyTabStops = 113,
stopRenderingLineAfter = 114,
suggest = 115,
suggestFontSize = 116,
suggestLineHeight = 117,
suggestOnTriggerCharacters = 118,
suggestSelection = 119,
tabCompletion = 120,
tabIndex = 121,
unicodeHighlighting = 122,
unusualLineTerminators = 123,
useShadowDOM = 124,
useTabStops = 125,
wordBreak = 126,
wordSeparators = 127,
wordWrap = 128,
wordWrapBreakAfterCharacters = 129,
wordWrapBreakBeforeCharacters = 130,
wordWrapColumn = 131,
wordWrapOverride1 = 132,
wordWrapOverride2 = 133,
wrappingIndent = 134,
wrappingStrategy = 135,
showDeprecated = 136,
inlayHints = 137,
editorClassName = 138,
pixelRatio = 139,
tabFocusMode = 140,
layoutInfo = 141,
wrappingInfo = 142,
defaultColorDecorators = 143,
colorDecoratorsActivatedOn = 144
}
/**
@ -920,4 +921,4 @@ export enum WrappingIndent {
* DeepIndent => wrapped lines get +2 indentation toward the parent.
*/
DeepIndent = 3
}
}

View file

@ -37,6 +37,15 @@
border: 1px solid var(--vscode-inputValidation-infoBorder);
}
.monaco-editor .monaco-editor-overlaymessage .message p {
margin-block: 0px;
}
.monaco-editor .monaco-editor-overlaymessage .message a {
font-weight: bold;
color: var(--vscode-inputValidation-infoForeground);
}
.monaco-editor.hc-black .monaco-editor-overlaymessage .message,
.monaco-editor.hc-light .monaco-editor-overlaymessage .message {
border-width: 2px;

View file

@ -3,8 +3,10 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { renderMarkdown } from 'vs/base/browser/markdownRenderer';
import { alert } from 'vs/base/browser/ui/aria/aria';
import { TimeoutTimer } from 'vs/base/common/async';
import { IMarkdownString, isMarkdownString } from 'vs/base/common/htmlContent';
import { KeyCode } from 'vs/base/common/keyCodes';
import { DisposableStore, IDisposable, MutableDisposable } from 'vs/base/common/lifecycle';
import 'vs/css!./messageController';
@ -14,9 +16,12 @@ import { IPosition } from 'vs/editor/common/core/position';
import { Range } from 'vs/editor/common/core/range';
import { IEditorContribution, ScrollType } from 'vs/editor/common/editorCommon';
import { PositionAffinity } from 'vs/editor/common/model';
import { openLinkFromMarkdown } from 'vs/editor/contrib/markdownRenderer/browser/markdownRenderer';
import * as nls from 'vs/nls';
import { IContextKey, IContextKeyService, RawContextKey } from 'vs/platform/contextkey/common/contextkey';
import { KeybindingWeight } from 'vs/platform/keybinding/common/keybindingsRegistry';
import { IOpenerService } from 'vs/platform/opener/common/opener';
import * as dom from 'vs/base/browser/dom';
export class MessageController implements IEditorContribution {
@ -32,10 +37,13 @@ export class MessageController implements IEditorContribution {
private readonly _visible: IContextKey<boolean>;
private readonly _messageWidget = new MutableDisposable<MessageWidget>();
private readonly _messageListeners = new DisposableStore();
private _message: { element: HTMLElement; dispose: () => void } | undefined;
private _focus: boolean = false;
constructor(
editor: ICodeEditor,
@IContextKeyService contextKeyService: IContextKeyService
@IContextKeyService contextKeyService: IContextKeyService,
@IOpenerService private readonly _openerService: IOpenerService
) {
this._editor = editor;
@ -43,6 +51,7 @@ export class MessageController implements IEditorContribution {
}
dispose(): void {
this._message?.dispose();
this._messageListeners.dispose();
this._messageWidget.dispose();
this._visible.reset();
@ -52,20 +61,33 @@ export class MessageController implements IEditorContribution {
return this._visible.get();
}
showMessage(message: string, position: IPosition): void {
showMessage(message: IMarkdownString | string, position: IPosition): void {
alert(message);
alert(isMarkdownString(message) ? message.value : message);
this._visible.set(true);
this._messageWidget.clear();
this._messageListeners.clear();
this._messageWidget.value = new MessageWidget(this._editor, position, message);
this._message = isMarkdownString(message) ? renderMarkdown(message, {
actionHandler: {
callback: (url) => openLinkFromMarkdown(this._openerService, url, isMarkdownString(message) ? message.isTrusted : undefined),
disposables: this._messageListeners
},
}) : undefined;
this._messageWidget.value = new MessageWidget(this._editor, position, typeof message === 'string' ? message : this._message!.element);
// close on blur, cursor, model change, dispose
this._messageListeners.add(this._editor.onDidBlurEditorText(() => this.closeMessage()));
this._messageListeners.add(this._editor.onDidBlurEditorText(() => {
if (!this._focus) {
this.closeMessage();
}
}
));
this._messageListeners.add(this._editor.onDidChangeCursorPosition(() => this.closeMessage()));
this._messageListeners.add(this._editor.onDidDispose(() => this.closeMessage()));
this._messageListeners.add(this._editor.onDidChangeModel(() => this.closeMessage()));
this._messageListeners.add(dom.addDisposableListener(this._messageWidget.value.getDomNode(), dom.EventType.MOUSE_ENTER, () => this._focus = true, true));
this._messageListeners.add(dom.addDisposableListener(this._messageWidget.value.getDomNode(), dom.EventType.MOUSE_LEAVE, () => this._focus = false, true));
// 3sec
this._messageListeners.add(new TimeoutTimer(() => this.closeMessage(), 3000));
@ -132,7 +154,7 @@ class MessageWidget implements IContentWidget {
return { dispose };
}
constructor(editor: ICodeEditor, { lineNumber, column }: IPosition, text: string) {
constructor(editor: ICodeEditor, { lineNumber, column }: IPosition, text: HTMLElement | string) {
this._editor = editor;
this._editor.revealLinesInCenterIfOutsideViewport(lineNumber, lineNumber, ScrollType.Smooth);
@ -147,8 +169,13 @@ class MessageWidget implements IContentWidget {
this._domNode.appendChild(anchorTop);
const message = document.createElement('div');
message.classList.add('message');
message.textContent = text;
if (typeof text === 'string') {
message.classList.add('message');
message.textContent = text;
} else {
text.classList.add('message');
message.appendChild(text);
}
this._domNode.appendChild(message);
const anchorBottom = document.createElement('div');

View file

@ -3,9 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { MarkdownString } from 'vs/base/common/htmlContent';
import { Disposable } from 'vs/base/common/lifecycle';
import { ICodeEditor } from 'vs/editor/browser/editorBrowser';
import { EditorContributionInstantiation, registerEditorContribution } from 'vs/editor/browser/editorExtensions';
import { EditorOption } from 'vs/editor/common/config/editorOptions';
import { IEditorContribution } from 'vs/editor/common/editorCommon';
import { MessageController } from 'vs/editor/contrib/message/browser/messageController';
import * as nls from 'vs/nls';
@ -24,11 +26,16 @@ export class ReadOnlyMessageController extends Disposable implements IEditorCont
private _onDidAttemptReadOnlyEdit(): void {
const messageController = MessageController.get(this.editor);
if (messageController && this.editor.hasModel()) {
if (this.editor.isSimpleWidget) {
messageController.showMessage(nls.localize('editor.simple.readonly', "Cannot edit in read-only input"), this.editor.getPosition());
} else {
messageController.showMessage(nls.localize('editor.readonly', "Cannot edit in read-only editor"), this.editor.getPosition());
let message = this.editor.getOptions().get(EditorOption.readOnlyMessage);
if (!message) {
if (this.editor.isSimpleWidget) {
message = new MarkdownString(nls.localize('editor.simple.readonly', "Cannot edit in read-only input"));
} else {
message = new MarkdownString(nls.localize('editor.readonly', "Cannot edit in read-only editor"));
}
}
messageController.showMessage(message, this.editor.getPosition());
}
}
}

116
src/vs/monaco.d.ts vendored
View file

@ -3309,6 +3309,10 @@ declare namespace monaco.editor {
* Defaults to false.
*/
readOnly?: boolean;
/**
* The message to display when the editor is readonly.
*/
readOnlyMessage?: IMarkdownString;
/**
* Should the textarea used for input use the DOM `readonly` attribute.
* Defaults to false.
@ -4934,61 +4938,62 @@ declare namespace monaco.editor {
quickSuggestions = 86,
quickSuggestionsDelay = 87,
readOnly = 88,
renameOnType = 89,
renderControlCharacters = 90,
renderFinalNewline = 91,
renderLineHighlight = 92,
renderLineHighlightOnlyWhenFocus = 93,
renderValidationDecorations = 94,
renderWhitespace = 95,
revealHorizontalRightPadding = 96,
roundedSelection = 97,
rulers = 98,
scrollbar = 99,
scrollBeyondLastColumn = 100,
scrollBeyondLastLine = 101,
scrollPredominantAxis = 102,
selectionClipboard = 103,
selectionHighlight = 104,
selectOnLineNumbers = 105,
showFoldingControls = 106,
showUnused = 107,
snippetSuggestions = 108,
smartSelect = 109,
smoothScrolling = 110,
stickyScroll = 111,
stickyTabStops = 112,
stopRenderingLineAfter = 113,
suggest = 114,
suggestFontSize = 115,
suggestLineHeight = 116,
suggestOnTriggerCharacters = 117,
suggestSelection = 118,
tabCompletion = 119,
tabIndex = 120,
unicodeHighlighting = 121,
unusualLineTerminators = 122,
useShadowDOM = 123,
useTabStops = 124,
wordBreak = 125,
wordSeparators = 126,
wordWrap = 127,
wordWrapBreakAfterCharacters = 128,
wordWrapBreakBeforeCharacters = 129,
wordWrapColumn = 130,
wordWrapOverride1 = 131,
wordWrapOverride2 = 132,
wrappingIndent = 133,
wrappingStrategy = 134,
showDeprecated = 135,
inlayHints = 136,
editorClassName = 137,
pixelRatio = 138,
tabFocusMode = 139,
layoutInfo = 140,
wrappingInfo = 141,
defaultColorDecorators = 142,
colorDecoratorsActivatedOn = 143
readOnlyMessage = 89,
renameOnType = 90,
renderControlCharacters = 91,
renderFinalNewline = 92,
renderLineHighlight = 93,
renderLineHighlightOnlyWhenFocus = 94,
renderValidationDecorations = 95,
renderWhitespace = 96,
revealHorizontalRightPadding = 97,
roundedSelection = 98,
rulers = 99,
scrollbar = 100,
scrollBeyondLastColumn = 101,
scrollBeyondLastLine = 102,
scrollPredominantAxis = 103,
selectionClipboard = 104,
selectionHighlight = 105,
selectOnLineNumbers = 106,
showFoldingControls = 107,
showUnused = 108,
snippetSuggestions = 109,
smartSelect = 110,
smoothScrolling = 111,
stickyScroll = 112,
stickyTabStops = 113,
stopRenderingLineAfter = 114,
suggest = 115,
suggestFontSize = 116,
suggestLineHeight = 117,
suggestOnTriggerCharacters = 118,
suggestSelection = 119,
tabCompletion = 120,
tabIndex = 121,
unicodeHighlighting = 122,
unusualLineTerminators = 123,
useShadowDOM = 124,
useTabStops = 125,
wordBreak = 126,
wordSeparators = 127,
wordWrap = 128,
wordWrapBreakAfterCharacters = 129,
wordWrapBreakBeforeCharacters = 130,
wordWrapColumn = 131,
wordWrapOverride1 = 132,
wordWrapOverride2 = 133,
wrappingIndent = 134,
wrappingStrategy = 135,
showDeprecated = 136,
inlayHints = 137,
editorClassName = 138,
pixelRatio = 139,
tabFocusMode = 140,
layoutInfo = 141,
wrappingInfo = 142,
defaultColorDecorators = 143,
colorDecoratorsActivatedOn = 144
}
export const EditorOptions: {
@ -5083,6 +5088,7 @@ declare namespace monaco.editor {
quickSuggestions: IEditorOption<EditorOption.quickSuggestions, InternalQuickSuggestionsOptions>;
quickSuggestionsDelay: IEditorOption<EditorOption.quickSuggestionsDelay, number>;
readOnly: IEditorOption<EditorOption.readOnly, boolean>;
readOnlyMessage: IEditorOption<EditorOption.readOnlyMessage, any>;
renameOnType: IEditorOption<EditorOption.renameOnType, boolean>;
renderControlCharacters: IEditorOption<EditorOption.renderControlCharacters, boolean>;
renderFinalNewline: IEditorOption<EditorOption.renderFinalNewline, 'on' | 'off' | 'dimmed'>;

View file

@ -18,6 +18,7 @@ import { localize } from 'vs/nls';
import { createDecorator } from 'vs/platform/instantiation/common/instantiation';
import { isWeb } from 'vs/base/common/platform';
import { Schemas } from 'vs/base/common/network';
import { IMarkdownString } from 'vs/base/common/htmlContent';
//#region file service & providers
@ -583,6 +584,7 @@ export const enum FileSystemProviderCapabilities {
export interface IFileSystemProvider {
readonly capabilities: FileSystemProviderCapabilities;
readonly readOnlyMessage?: IMarkdownString;
readonly onDidChangeCapabilities: Event<void>;
readonly onDidChangeFile: Event<readonly IFileChange[]>;

View file

@ -17,6 +17,7 @@ import { IWorkbenchFileService } from 'vs/workbench/services/files/common/files'
import { normalizeWatcherPattern } from 'vs/platform/files/common/watcher';
import { GLOBSTAR } from 'vs/base/common/glob';
import { rtrim } from 'vs/base/common/strings';
import { IMarkdownString } from 'vs/base/common/htmlContent';
@extHostNamedCustomer(MainContext.MainThreadFileSystem)
export class MainThreadFileSystem implements MainThreadFileSystemShape {
@ -50,8 +51,8 @@ export class MainThreadFileSystem implements MainThreadFileSystemShape {
this._watches.dispose();
}
async $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities): Promise<void> {
this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, handle, this._proxy));
async $registerFileSystemProvider(handle: number, scheme: string, capabilities: FileSystemProviderCapabilities, readonlyMessage?: IMarkdownString): Promise<void> {
this._fileProvider.set(handle, new RemoteFileSystemProvider(this._fileService, scheme, capabilities, readonlyMessage, handle, this._proxy));
}
$unregisterProvider(handle: number): void {
@ -273,6 +274,7 @@ class RemoteFileSystemProvider implements IFileSystemProviderWithFileReadWriteCa
fileService: IFileService,
scheme: string,
capabilities: FileSystemProviderCapabilities,
public readonly readOnlyMessage: IMarkdownString | undefined,
private readonly _handle: number,
private readonly _proxy: ExtHostFileSystemShape
) {

View file

@ -1236,7 +1236,7 @@ export interface IFileChangeDto {
}
export interface MainThreadFileSystemShape extends IDisposable {
$registerFileSystemProvider(handle: number, scheme: string, capabilities: files.FileSystemProviderCapabilities): Promise<void>;
$registerFileSystemProvider(handle: number, scheme: string, capabilities: files.FileSystemProviderCapabilities, readonlyMessage?: IMarkdownString): Promise<void>;
$unregisterProvider(handle: number): void;
$onFileSystemChange(handle: number, resource: IFileChangeDto[]): void;

View file

@ -17,6 +17,7 @@ import { CharCode } from 'vs/base/common/charCode';
import { VSBuffer } from 'vs/base/common/buffer';
import { IExtensionDescription } from 'vs/platform/extensions/common/extensions';
import { checkProposedApiEnabled } from 'vs/workbench/services/extensions/common/extensions';
import { IMarkdownString, isMarkdownString } from 'vs/base/common/htmlContent';
class FsLinkProvider {
@ -128,7 +129,7 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape {
this._linkProviderRegistration?.dispose();
}
registerFileSystemProvider(extension: IExtensionDescription, scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean; isReadonly?: boolean } = {}) {
registerFileSystemProvider(extension: IExtensionDescription, scheme: string, provider: vscode.FileSystemProvider, options: { isCaseSensitive?: boolean; isReadonly?: boolean | vscode.MarkdownString } = {}) {
// validate the given provider is complete
ExtHostFileSystem._validateFileSystemProvider(provider);
@ -164,7 +165,20 @@ export class ExtHostFileSystem implements ExtHostFileSystemShape {
capabilities += files.FileSystemProviderCapabilities.FileOpenReadWriteClose;
}
this._proxy.$registerFileSystemProvider(handle, scheme, capabilities).catch(err => {
let readOnlyMessage: IMarkdownString | undefined;
if (options.isReadonly && isMarkdownString(options.isReadonly)) {
checkProposedApiEnabled(extension, 'readonlyMessage');
readOnlyMessage = {
value: options.isReadonly.value,
isTrusted: options.isReadonly.isTrusted,
supportThemeIcons: options.isReadonly.supportThemeIcons,
supportHtml: options.isReadonly.supportHtml,
baseUri: options.isReadonly.baseUri,
uris: options.isReadonly.uris
};
}
this._proxy.$registerFileSystemProvider(handle, scheme, capabilities, readOnlyMessage).catch(err => {
console.error(`FAILED to register filesystem provider of ${extension.identifier.value}-extension for the scheme ${scheme}`);
console.error(err);
});

View file

@ -17,6 +17,7 @@ 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.
@ -53,6 +54,12 @@ 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) {

View file

@ -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({
readOnly: resolvedDiffEditorModel.modifiedModel?.isReadonly(),
...this.readonlyValues(resolvedDiffEditorModel.modifiedModel?.isReadonly()),
originalEditable: !resolvedDiffEditorModel.originalModel?.isReadonly()
});

View file

@ -144,13 +144,11 @@ export abstract class AbstractTextEditor<T extends IEditorViewState> extends Abs
}
protected getConfigurationOverrides(): ICodeEditorOptions {
const readOnly = this.input?.hasCapability(EditorInputCapabilities.Readonly);
return {
overviewRulerLanes: 3,
lineNumbersMinChars: 3,
fixedOverflowWidgets: true,
readOnly,
...this.readonlyValues(this.input?.isReadonly()),
renderValidationDecorations: 'on' // render problems even in readonly editors (https://github.com/microsoft/vscode/issues/89057)
};
}

View file

@ -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({ readOnly: resolvedModel.isReadonly() });
control.updateOptions(this.readonlyValues(resolvedModel.isReadonly()));
}
/**

View file

@ -10,6 +10,7 @@ import { firstOrDefault } from 'vs/base/common/arrays';
import { EditorInputCapabilities, Verbosity, GroupIdentifier, ISaveOptions, IRevertOptions, IMoveResult, IEditorDescriptor, IEditorPane, IUntypedEditorInput, EditorResourceAccessor, AbstractEditorInput, isEditorInput, IEditorIdentifier } from 'vs/workbench/common/editor';
import { isEqual } from 'vs/base/common/resources';
import { ConfirmResult } from 'vs/platform/dialogs/common/dialogs';
import { IMarkdownString } from 'vs/base/common/htmlContent';
export interface IEditorCloseHandler {
@ -124,6 +125,10 @@ export abstract class EditorInput extends AbstractEditorInput {
return (this.capabilities & capability) !== 0;
}
isReadonly(): boolean | IMarkdownString | undefined {
return this.hasCapability(EditorInputCapabilities.Readonly);
}
/**
* Returns the display name of this input.
*/

View file

@ -6,6 +6,7 @@
import { IDiffEditorModel } from 'vs/editor/common/editorCommon';
import { BaseTextEditorModel } from 'vs/workbench/common/editor/textEditorModel';
import { DiffEditorModel } from 'vs/workbench/common/editor/diffEditorModel';
import { IMarkdownString } from 'vs/base/common/htmlContent';
/**
* The base text editor model for the diff editor. It is made up of two text editor models, the original version
@ -60,7 +61,7 @@ export class TextDiffEditorModel extends DiffEditorModel {
return !!this._textDiffEditorModel;
}
isReadonly(): boolean {
isReadonly(): boolean | IMarkdownString {
return !!this.modifiedModel && this.modifiedModel.isReadonly();
}

View file

@ -17,6 +17,7 @@ import { ILanguageDetectionService, LanguageDetectionLanguageEventSource } from
import { ThrottledDelayer } from 'vs/base/common/async';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { localize } from 'vs/nls';
import { IMarkdownString } from 'vs/base/common/htmlContent';
/**
* The base text editor model leverages the code editor model. This class is only intended to be subclassed and not instantiated.
@ -71,7 +72,7 @@ export class BaseTextEditorModel extends EditorModel implements ITextEditorModel
return this.textEditorModelHandle ? this.modelService.getModel(this.textEditorModelHandle) : null;
}
isReadonly(): boolean {
isReadonly(): boolean | IMarkdownString {
return true;
}

View file

@ -64,7 +64,7 @@ export class CustomTextEditorModel extends Disposable implements ICustomEditorMo
}
public isReadonly(): boolean {
return this._model.object.isReadonly();
return !!this._model.object.isReadonly();
}
public get backupId() {

View file

@ -25,6 +25,7 @@ import { createTextBufferFactory } from 'vs/editor/common/model/textModel';
import { IPathService } from 'vs/workbench/services/path/common/pathService';
import { ITextResourceConfigurationService } from 'vs/editor/common/services/textResourceConfiguration';
import { isConfigured } from 'vs/platform/configuration/common/configuration';
import { IMarkdownString } from 'vs/base/common/htmlContent';
const enum ForceOpenAs {
None,
@ -192,6 +193,10 @@ export class FileEditorInput extends AbstractTextResourceEditorInput implements
return this.preferredName;
}
override isReadonly(): boolean | IMarkdownString | undefined {
return this.model ? this.model.isReadonly() : super.isReadonly();
}
override getDescription(verbosity?: Verbosity): string | undefined {
return this.preferredDescription || super.getDescription(verbosity);
}

View file

@ -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({ readOnly: textFileModel.isReadonly() });
control.updateOptions(this.readonlyValues(textFileModel.isReadonly()));
} catch (error) {
await this.handleSetInputError(error, input, options);
}

View file

@ -149,7 +149,7 @@ export class ExplorerItem {
}
get isReadonly(): boolean {
return this.filesConfigService.isReadonly(this.resource, { resource: this.resource, name: this.name, readonly: this._readonly, locked: this._locked });
return !!this.filesConfigService.isReadonly(this.resource, { resource: this.resource, name: this.name, readonly: this._readonly, locked: this._locked });
}
get mtime(): number | undefined {

View file

@ -795,7 +795,7 @@ export interface INotebookEditorModel extends IEditorModel {
readonly notebook: INotebookTextModel | undefined;
isResolved(): this is IResolvedNotebookEditorModel;
isDirty(): boolean;
isReadonly(): boolean;
isReadonly(): boolean | IMarkdownString;
isOrphaned(): boolean;
hasAssociatedFilePath(): boolean;
load(options?: INotebookLoadOptions): Promise<IResolvedNotebookEditorModel>;

View file

@ -7,6 +7,7 @@ import { VSBufferReadableStream, bufferToStream, streamToBuffer } from 'vs/base/
import { CancellationToken } from 'vs/base/common/cancellation';
import { CancellationError } from 'vs/base/common/errors';
import { Emitter, Event } from 'vs/base/common/event';
import { IMarkdownString } from 'vs/base/common/htmlContent';
import { Disposable, DisposableStore } from 'vs/base/common/lifecycle';
import { Schemas } from 'vs/base/common/network';
import { filter } from 'vs/base/common/objects';
@ -100,13 +101,11 @@ export class SimpleNotebookEditorModel extends EditorModel implements INotebookE
return !SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy) && !!this._workingCopy?.hasAssociatedFilePath;
}
isReadonly(): boolean {
isReadonly(): boolean | IMarkdownString {
if (SimpleNotebookEditorModel._isStoredFileWorkingCopy(this._workingCopy)) {
return this._workingCopy?.isReadonly();
} else if (this._filesConfigurationService.isReadonly(this.resource)) {
return true;
} else {
return false;
return this._filesConfigurationService.isReadonly(this.resource);
}
}

View file

@ -69,6 +69,7 @@ export const allApiProposals = Object.freeze({
quickPickItemIcon: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemIcon.d.ts',
quickPickItemTooltip: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickItemTooltip.d.ts',
quickPickSortByLabel: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.quickPickSortByLabel.d.ts',
readonlyMessage: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.readonlyMessage.d.ts',
resolvers: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.resolvers.d.ts',
saveEditor: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.saveEditor.d.ts',
scmActionButton: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.scmActionButton.d.ts',

View file

@ -20,6 +20,7 @@ import { IUriIdentityService } from 'vs/platform/uriIdentity/common/uriIdentity'
import { IEnvironmentService } from 'vs/platform/environment/common/environment';
import { ResourceMap } from 'vs/base/common/map';
import { withNullAsUndefined } from 'vs/base/common/types';
import { IMarkdownString } from 'vs/base/common/htmlContent';
export const AutoSaveAfterShortDelayContext = new RawContextKey<boolean>('autoSaveAfterShortDelayContext', false, true);
@ -59,7 +60,7 @@ export interface IFilesConfigurationService {
readonly onReadonlyChange: Event<void>;
isReadonly(resource: URI, stat?: IBaseFileStat): boolean;
isReadonly(resource: URI, stat?: IBaseFileStat): boolean | IMarkdownString;
updateReadonly(resource: URI, readonly: true | false | 'toggle' | 'reset'): Promise<void>;
@ -103,7 +104,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>(resource => this.uriIdentityService.extUri.getComparisonKey(resource));
private readonly sessionReadonlyOverrides = new ResourceMap<boolean | IMarkdownString>(resource => this.uriIdentityService.extUri.getComparisonKey(resource));
constructor(
@IContextKeyService contextKeyService: IContextKeyService,
@ -140,13 +141,13 @@ export class FilesConfigurationService extends Disposable implements IFilesConfi
return matcher;
}
isReadonly(resource: URI, stat?: IBaseFileStat): boolean {
isReadonly(resource: URI, stat?: IBaseFileStat): boolean | IMarkdownString {
// 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 true;
return this.fileService.getProvider(resource.scheme)?.readOnlyMessage ?? true;
}
// session override always wins over the others

View file

@ -32,6 +32,7 @@ import { extUri } from 'vs/base/common/resources';
import { IAccessibilityService } from 'vs/platform/accessibility/common/accessibility';
import { PLAINTEXT_LANGUAGE_ID } from 'vs/editor/common/languages/modesRegistry';
import { IExtensionService } from 'vs/workbench/services/extensions/common/extensions';
import { IMarkdownString } from 'vs/base/common/htmlContent';
interface IBackupMetaData extends IWorkingCopyBackupMeta {
mtime: number;
@ -1163,7 +1164,7 @@ export class TextFileEditorModel extends BaseTextEditorModel implements ITextFil
return !!this.textEditorModel;
}
override isReadonly(): boolean {
override isReadonly(): boolean | IMarkdownString {
return this.filesConfigurationService.isReadonly(this.resource, this.lastResolvedFileStat);
}

View file

@ -28,6 +28,7 @@ import { IEditorService } from 'vs/workbench/services/editor/common/editorServic
import { IElevatedFileService } from 'vs/workbench/services/files/common/elevatedFileService';
import { IResourceWorkingCopy, ResourceWorkingCopy } from 'vs/workbench/services/workingCopy/common/resourceWorkingCopy';
import { IFileWorkingCopy, IFileWorkingCopyModel, IFileWorkingCopyModelFactory } from 'vs/workbench/services/workingCopy/common/fileWorkingCopy';
import { IMarkdownString } from 'vs/base/common/htmlContent';
/**
* Stored file specific working copy model factory.
@ -145,7 +146,7 @@ export interface IStoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> e
/**
* Whether the stored file working copy is readonly or not.
*/
isReadonly(): boolean;
isReadonly(): boolean | IMarkdownString;
}
export interface IResolvedStoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extends IStoredFileWorkingCopy<M> {
@ -1247,7 +1248,7 @@ export class StoredFileWorkingCopy<M extends IStoredFileWorkingCopyModel> extend
//#region Utilities
isReadonly(): boolean {
isReadonly(): boolean | IMarkdownString {
return this.filesConfigurationService.isReadonly(this.resource, this.lastResolvedFileStat);
}

View file

@ -0,0 +1,14 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
// https://github.com/microsoft/vscode/issues/166971
declare module 'vscode' {
export namespace workspace {
export function registerFileSystemProvider(scheme: string, provider: FileSystemProvider, options?: { readonly isCaseSensitive?: boolean; readonly isReadonly?: boolean | MarkdownString }): Disposable;
}
}