Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve readonly editor behavior #13403

Merged
merged 2 commits into from
Feb 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions .gitpod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,10 @@ ports:
- port: 9339 # Node.js debug port
onOpen: ignore
tasks:
- init: yarn --network-timeout 100000 && yarn build:examples && yarn download:plugins
- init: yarn --network-timeout 100000 && yarn browser build && yarn download:plugins
command: >
jwm &
yarn --cwd examples/browser start ../.. --hostname=0.0.0.0
github:
prebuilds:
pullRequestsFromForks: true
yarn browser start ../.. --hostname=0.0.0.0
vscode:
extensions:
- dbaeumer.vscode-eslint@2.0.0:CwAMx4wYz1Kq39+1Aul4VQ==
- dbaeumer.vscode-eslint
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { bindMonacoPreferenceExtractor } from './monaco-editor-preferences/monac
import { rebindOVSXClientFactory } from '../common/vsx/sample-ovsx-client-factory';
import { bindSampleAppInfo } from './vsx/sample-frontend-app-info';
import { bindTestSample } from './test/sample-test-contribution';
import { bindSampleFileSystemCapabilitiesCommands } from './file-system/sample-file-system-capabilities';

export default new ContainerModule((
bind: interfaces.Bind,
Expand All @@ -47,5 +48,6 @@ export default new ContainerModule((
bindMonacoPreferenceExtractor(bind);
bindSampleAppInfo(bind);
bindTestSample(bind);
bindSampleFileSystemCapabilitiesCommands(bind);
rebindOVSXClientFactory(rebind);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/********************************************************************************
* Copyright (C) 2024 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
********************************************************************************/

import { CommandContribution, CommandRegistry } from '@theia/core';
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
import { RemoteFileSystemProvider } from '@theia/filesystem/lib/common/remote-file-system-provider';
import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files';

@injectable()
export class SampleFileSystemCapabilities implements CommandContribution {

@inject(RemoteFileSystemProvider)
protected readonly remoteFileSystemProvider: RemoteFileSystemProvider;

registerCommands(commands: CommandRegistry): void {
commands.registerCommand({
id: 'toggleFileSystemReadonly',
label: 'Toggle File System Readonly'
}, {
execute: () => {
const readonly = (this.remoteFileSystemProvider.capabilities & FileSystemProviderCapabilities.Readonly) !== 0;
if (readonly) {
this.remoteFileSystemProvider['setCapabilities'](this.remoteFileSystemProvider.capabilities & ~FileSystemProviderCapabilities.Readonly);
} else {
this.remoteFileSystemProvider['setCapabilities'](this.remoteFileSystemProvider.capabilities | FileSystemProviderCapabilities.Readonly);
}
}
});
}

}

export function bindSampleFileSystemCapabilitiesCommands(bind: interfaces.Bind): void {
bind(CommandContribution).to(SampleFileSystemCapabilities).inSingletonScope();
}
8 changes: 8 additions & 0 deletions packages/core/src/browser/widgets/widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -381,12 +381,20 @@ export function pin(title: Title<Widget>): void {
}
}

export function isLocked(title: Title<Widget>): boolean {
return title.className.includes(LOCKED_CLASS);
}

export function lock(title: Title<Widget>): void {
if (!title.className.includes(LOCKED_CLASS)) {
title.className += ` ${LOCKED_CLASS}`;
}
}

export function unlock(title: Title<Widget>): void {
title.className = title.className.replace(LOCKED_CLASS, '').trim();
}

export function togglePinned(title?: Title<Widget>): void {
if (title) {
if (isPinned(title)) {
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/common/resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import { CancellationToken } from './cancellation';
import { ApplicationError } from './application-error';
import { ReadableStream, Readable } from './stream';
import { SyncReferenceCollection, Reference } from './reference';
import { MarkdownString } from './markdown-rendering';

export interface ResourceVersion {
}
Expand Down Expand Up @@ -55,7 +56,10 @@ export interface Resource extends Disposable {
* Undefined if a resource did not read content yet.
*/
readonly encoding?: string | undefined;
readonly isReadonly?: boolean;

readonly onDidChangeReadOnly?: Event<boolean | MarkdownString>;

readonly readOnly?: boolean | MarkdownString;
/**
* Reads latest content of this resource.
*
Expand Down
9 changes: 8 additions & 1 deletion packages/editor/src/browser/editor-widget.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
// *****************************************************************************

import { Disposable, SelectionService, Event, UNTITLED_SCHEME, DisposableCollection } from '@theia/core/lib/common';
import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget, lock, TabBar, DockPanel } from '@theia/core/lib/browser';
import { Widget, BaseWidget, Message, Saveable, SaveableSource, Navigatable, StatefulWidget, lock, TabBar, DockPanel, unlock } from '@theia/core/lib/browser';
import URI from '@theia/core/lib/common/uri';
import { find } from '@theia/core/shared/@phosphor/algorithm';
import { TextEditor } from './editor';
Expand All @@ -38,6 +38,13 @@ export class EditorWidget extends BaseWidget implements SaveableSource, Navigata
this.toDispose.push(this.toDisposeOnTabbarChange);
this.toDispose.push(this.editor.onSelectionChanged(() => this.setSelection()));
this.toDispose.push(this.editor.onFocusChanged(() => this.setSelection()));
this.toDispose.push(this.editor.onDidChangeReadOnly(isReadonly => {
if (isReadonly) {
lock(this.title);
} else {
unlock(this.title);
}
}));
this.toDispose.push(Disposable.create(() => {
if (this.selectionService.selection === this.editor) {
this.selectionService.selection = undefined;
Expand Down
4 changes: 3 additions & 1 deletion packages/editor/src/browser/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import URI from '@theia/core/lib/common/uri';
import { Event, Disposable, TextDocumentContentChangeDelta, Reference, isObject } from '@theia/core/lib/common';
import { Saveable, Navigatable, Widget } from '@theia/core/lib/browser';
import { EditorDecoration } from './decorations/editor-decoration';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';

export { Position, Range, Location };

Expand Down Expand Up @@ -207,7 +208,8 @@ export interface TextEditor extends Disposable, TextEditorSelection, Navigatable
readonly node: HTMLElement;

readonly uri: URI;
readonly isReadonly: boolean;
readonly isReadonly: boolean | MarkdownString;
readonly onDidChangeReadOnly: Event<boolean | MarkdownString>;
readonly document: TextEditorDocument;
readonly onDocumentContentChanged: Event<TextDocumentChangeEvent>;

Expand Down
16 changes: 14 additions & 2 deletions packages/filesystem/src/browser/file-resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import { LabelProvider } from '@theia/core/lib/browser/label-provider';
import { GENERAL_MAX_FILE_SIZE_MB } from './filesystem-preferences';
import { FrontendApplicationStateService } from '@theia/core/lib/browser/frontend-application-state';
import { nls } from '@theia/core';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';

export interface FileResourceVersion extends ResourceVersion {
readonly encoding: string;
Expand Down Expand Up @@ -54,14 +55,17 @@ export class FileResource implements Resource {
protected readonly onDidChangeContentsEmitter = new Emitter<void>();
readonly onDidChangeContents: Event<void> = this.onDidChangeContentsEmitter.event;

protected readonly onDidChangeReadOnlyEmitter = new Emitter<boolean | MarkdownString>();
readonly onDidChangeReadOnly: Event<boolean | MarkdownString> = this.onDidChangeReadOnlyEmitter.event;

protected _version: FileResourceVersion | undefined;
get version(): FileResourceVersion | undefined {
return this._version;
}
get encoding(): string | undefined {
return this._version?.encoding;
}
get isReadonly(): boolean {
get readOnly(): boolean {
return this.options.isReadonly || this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Readonly);
}

Expand All @@ -71,6 +75,7 @@ export class FileResource implements Resource {
protected readonly options: FileResourceOptions
) {
this.toDispose.push(this.onDidChangeContentsEmitter);
this.toDispose.push(this.onDidChangeReadOnlyEmitter);
this.toDispose.push(this.fileService.onDidFilesChange(event => {
if (event.contains(this.uri)) {
this.sync();
Expand Down Expand Up @@ -220,17 +225,24 @@ export class FileResource implements Resource {
saveContents?: Resource['saveContents'];
saveContentChanges?: Resource['saveContentChanges'];
protected updateSavingContentChanges(): void {
if (this.isReadonly) {
let changed = false;
if (this.readOnly) {
changed = Boolean(this.saveContents);
delete this.saveContentChanges;
delete this.saveContents;
delete this.saveStream;
} else {
changed = !Boolean(this.saveContents);
this.saveContents = this.doWrite;
this.saveStream = this.doWrite;
if (this.fileService.hasCapability(this.uri, FileSystemProviderCapabilities.Update)) {
this.saveContentChanges = this.doSaveContentChanges;
}
}
if (changed) {
// Only actually bother to call the event if the value has changed.
this.onDidChangeReadOnlyEmitter.fire(this.readOnly);
}
}
protected doSaveContentChanges: Resource['saveContentChanges'] = async (changes, options) => {
const version = options?.version || this._version;
Expand Down
9 changes: 6 additions & 3 deletions packages/monaco/src/browser/monaco-editor-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { ILanguageService } from '@theia/monaco-editor-core/esm/vs/editor/common
import { IModelService } from '@theia/monaco-editor-core/esm/vs/editor/common/services/model';
import { createTextBufferFactoryFromStream } from '@theia/monaco-editor-core/esm/vs/editor/common/model/textModel';
import { editorGeneratedPreferenceProperties } from '@theia/editor/lib/browser/editor-generated-preference-schema';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';

export {
TextDocumentSaveReason
Expand Down Expand Up @@ -81,6 +82,8 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
protected readonly onDidChangeEncodingEmitter = new Emitter<string>();
readonly onDidChangeEncoding = this.onDidChangeEncodingEmitter.event;

readonly onDidChangeReadOnly: Event<boolean | MarkdownString> = this.resource.onDidChangeReadOnly ?? Event.None;

private preferredEncoding: string | undefined;
private contentEncoding: string | undefined;

Expand Down Expand Up @@ -302,11 +305,11 @@ export class MonacoEditorModel implements IResolvedTextEditorModel, TextEditorDo
return this.m2p.asRange(this.model.validateRange(this.p2m.asRange(range)));
}

get readOnly(): boolean {
return this.resource.saveContents === undefined;
get readOnly(): boolean | MarkdownString {
return this.resource.readOnly ?? false;
}

isReadonly(): boolean {
isReadonly(): boolean | MarkdownString {
return this.readOnly;
}

Expand Down
3 changes: 0 additions & 3 deletions packages/monaco/src/browser/monaco-editor-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import { MonacoDiffNavigatorFactory } from './monaco-diff-navigator-factory';
import { EditorServiceOverrides, MonacoEditor, MonacoEditorServices } from './monaco-editor';
import { MonacoEditorModel, WillSaveMonacoModelEvent } from './monaco-editor-model';
import { MonacoWorkspace } from './monaco-workspace';
import { ApplicationServer } from '@theia/core/lib/common/application-protocol';
import { ContributionProvider } from '@theia/core';
import { KeybindingRegistry, OpenerService, open, WidgetOpenerOptions, FormatType } from '@theia/core/lib/browser';
import { MonacoResolvedKeybinding } from './monaco-resolved-keybinding';
Expand Down Expand Up @@ -86,8 +85,6 @@ export class MonacoEditorProvider {
@inject(MonacoWorkspace) protected readonly workspace: MonacoWorkspace,
@inject(EditorPreferences) protected readonly editorPreferences: EditorPreferences,
@inject(MonacoDiffNavigatorFactory) protected readonly diffNavigatorFactory: MonacoDiffNavigatorFactory,
/** @deprecated since 1.6.0 */
@inject(ApplicationServer) protected readonly applicationServer: ApplicationServer,
) {
}

Expand Down
24 changes: 21 additions & 3 deletions packages/monaco/src/browser/monaco-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { IInstantiationService, ServiceIdentifier } from '@theia/monaco-editor-c
import { ICodeEditor, IMouseTargetMargin } from '@theia/monaco-editor-core/esm/vs/editor/browser/editorBrowser';
import { IStandaloneEditorConstructionOptions, StandaloneEditor } from '@theia/monaco-editor-core/esm/vs/editor/standalone/browser/standaloneCodeEditor';
import { ServiceCollection } from '@theia/monaco-editor-core/esm/vs/platform/instantiation/common/serviceCollection';
import { MarkdownString } from '@theia/core/lib/common/markdown-rendering';

export type ServicePair<T> = [ServiceIdentifier<T>, T];

Expand Down Expand Up @@ -86,6 +87,7 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor {
protected readonly onFocusChangedEmitter = new Emitter<boolean>();
protected readonly onDocumentContentChangedEmitter = new Emitter<TextDocumentChangeEvent>();
protected readonly onMouseDownEmitter = new Emitter<EditorMouseEvent>();
readonly onDidChangeReadOnly = this.document.onDidChangeReadOnly;
protected readonly onLanguageChangedEmitter = new Emitter<string>();
readonly onLanguageChanged = this.onLanguageChangedEmitter.event;
protected readonly onScrollChangedEmitter = new Emitter<void>();
Expand Down Expand Up @@ -117,7 +119,10 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor {
this.autoSizing = options && options.autoSizing !== undefined ? options.autoSizing : false;
this.minHeight = options && options.minHeight !== undefined ? options.minHeight : -1;
this.maxHeight = options && options.maxHeight !== undefined ? options.maxHeight : -1;
this.toDispose.push(this.create(options, override));
this.toDispose.push(this.create({
...MonacoEditor.createReadOnlyOptions(document.readOnly),
...options
}, override));
this.addHandlers(this.editor);
}

Expand Down Expand Up @@ -199,6 +204,9 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor {
this.toDispose.push(codeEditor.onDidScrollChange(e => {
this.onScrollChangedEmitter.fire(undefined);
}));
this.toDispose.push(this.onDidChangeReadOnly(readOnly => {
codeEditor.updateOptions(MonacoEditor.createReadOnlyOptions(readOnly));
}));
}

getVisibleRanges(): Range[] {
Expand All @@ -221,8 +229,8 @@ export class MonacoEditor extends MonacoEditorServices implements TextEditor {
return this.onDocumentContentChangedEmitter.event;
}

get isReadonly(): boolean {
return this.document.isReadonly();
get isReadonly(): boolean | MarkdownString {
return this.document.readOnly;
}

get cursor(): Position {
Expand Down Expand Up @@ -642,4 +650,14 @@ export namespace MonacoEditor {
return candidate && candidate.getControl() === control;
});
}

export function createReadOnlyOptions(readOnly?: boolean | MarkdownString): monaco.editor.IEditorOptions {
if (typeof readOnly === 'boolean') {
return { readOnly };
}
if (readOnly) {
return { readOnly: true, readOnlyMessage: readOnly };
}
return {};
}
}
9 changes: 8 additions & 1 deletion packages/monaco/src/browser/simple-monaco-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab
readonly onEncodingChanged = this.document.onDidChangeEncoding;
protected readonly onResizeEmitter = new Emitter<Dimension | null>();
readonly onDidResize = this.onResizeEmitter.event;
readonly onDidChangeReadOnly = this.document.onDidChangeReadOnly;

constructor(
readonly uri: URI,
Expand All @@ -62,7 +63,10 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab
this.onLanguageChangedEmitter,
this.onScrollChangedEmitter
]);
this.toDispose.push(this.create(options, override));
this.toDispose.push(this.create({
...MonacoEditor.createReadOnlyOptions(document.readOnly),
...options
}, override));
this.addHandlers(this.editor);
this.editor.setModel(document.textEditorModel);
}
Expand Down Expand Up @@ -125,6 +129,9 @@ export class SimpleMonacoEditor extends MonacoEditorServices implements Disposab
this.toDispose.push(codeEditor.onDidScrollChange(e => {
this.onScrollChangedEmitter.fire(undefined);
}));
this.toDispose.push(this.onDidChangeReadOnly(readOnly => {
codeEditor.updateOptions(MonacoEditor.createReadOnlyOptions(readOnly));
}));
}

setLanguage(languageId: string): void {
Expand Down
Loading
Loading