Skip to content

Commit

Permalink
6636-custom-editor: Adds a prototype of custom editors contributed by…
Browse files Browse the repository at this point in the history
… extensions with this functionality:

- Adds a new contribution point for custom editors.
- Adds API for registering a custom editor providers.
- Implements CustomEditor extension API - based on VSCode (excluding backup functionality not implemented in this PR).
- Adds CustomEditorWidget extending WebviewWidget containing a model reference to CustomEditorModel.
- Supports two CustomEditorModel implementations: CustomTextEditorModel for text documents and MainCustomEditorModel for binary documents.
- Registers openHandlers for CustomEditors.
- Adds `openWith` command for selecting which editor to use when openning a resource.
- Adds Undo/Redo functionality for CustomEditors.

Signed-off-by: Dan Arad <dan.arad@sap.com>
  • Loading branch information
danarad05 committed Feb 2, 2021
1 parent 4a3e133 commit 3639693
Show file tree
Hide file tree
Showing 35 changed files with 3,281 additions and 68 deletions.
26 changes: 24 additions & 2 deletions packages/core/src/browser/opener-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

import { named, injectable, inject } from 'inversify';
import URI from '../common/uri';
import { ContributionProvider, Prioritizeable, MaybePromise } from '../common';
import { ContributionProvider, Prioritizeable, MaybePromise, Emitter, Event, Disposable } from '../common';

export interface OpenerOptions {
}
Expand Down Expand Up @@ -75,6 +75,10 @@ export interface OpenerService {
* Reject if such does not exist.
*/
getOpener(uri: URI, options?: OpenerOptions): Promise<OpenHandler>;
/**
* Event that fires when a new opener is added or removed.
*/
onOpenersStateChanged?: Event<void>;
}

export async function open(openerService: OpenerService, uri: URI, options?: OpenerOptions): Promise<object | undefined> {
Expand All @@ -84,12 +88,27 @@ export async function open(openerService: OpenerService, uri: URI, options?: Ope

@injectable()
export class DefaultOpenerService implements OpenerService {
// Collection of open-handlers for custom-editor contributions.
protected readonly customEditorOpenHandlers: OpenHandler[] = [];

protected readonly onOpenersStateChangedEmitter = new Emitter<void>();
readonly onOpenersStateChanged = this.onOpenersStateChangedEmitter.event;

constructor(
@inject(ContributionProvider) @named(OpenHandler)
protected readonly handlersProvider: ContributionProvider<OpenHandler>
) { }

public addHandler(openHandler: OpenHandler): Disposable {
this.customEditorOpenHandlers.push(openHandler);
this.onOpenersStateChangedEmitter.fire();

return Disposable.create(() => {
this.customEditorOpenHandlers.splice(this.customEditorOpenHandlers.indexOf(openHandler), 1);
this.onOpenersStateChangedEmitter.fire();
});
}

async getOpener(uri: URI, options?: OpenerOptions): Promise<OpenHandler> {
const handlers = await this.prioritize(uri, options);
if (handlers.length >= 1) {
Expand All @@ -114,7 +133,10 @@ export class DefaultOpenerService implements OpenerService {
}

protected getHandlers(): OpenHandler[] {
return this.handlersProvider.getContributions();
return [
...this.handlersProvider.getContributions(),
...this.customEditorOpenHandlers
];
}

}
2 changes: 1 addition & 1 deletion packages/core/src/browser/saveable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export namespace Saveable {
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function isSource(arg: any): arg is SaveableSource {
return !!arg && ('saveable' in arg);
return !!arg && ('saveable' in arg) && is(arg.saveable);
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function is(arg: any): arg is Saveable {
Expand Down
14 changes: 13 additions & 1 deletion packages/editor/src/browser/editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import { Position, Range, Location } from 'vscode-languageserver-types';
import * as lsp from 'vscode-languageserver-types';
import URI from '@theia/core/lib/common/uri';
import { Event, Disposable, TextDocumentContentChangeDelta } from '@theia/core/lib/common';
import { Saveable, Navigatable } from '@theia/core/lib/browser';
import { Saveable, Navigatable, Widget } from '@theia/core/lib/browser';
import { EditorDecoration } from './decorations';
import { Reference } from '@theia/core/lib/common';

export {
Position, Range, Location
Expand Down Expand Up @@ -336,3 +337,14 @@ export namespace TextEditorSelection {
return e && e['uri'] instanceof URI;
}
}

export namespace CustomEditorWidget {
export function is(arg: Widget | undefined): arg is CustomEditorWidget {
return !!arg && 'modelRef' in arg;
}
}

export interface CustomEditorWidget extends Widget {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
readonly modelRef: Reference<any>;
}
3 changes: 3 additions & 0 deletions packages/monaco/src/browser/monaco-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,9 @@ export class MonacoEditorCommandHandlers implements CommandContribution {
},
isEnabled: () => {
const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor();
if (!editor) {
return false;
}
if (editorActions.has(id)) {
const action = editor && editor.getAction(id);
return !!action && action.isSupported();
Expand Down
11 changes: 9 additions & 2 deletions packages/monaco/src/browser/monaco-editor-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,10 @@
import { injectable, inject, decorate } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { OpenerService, open, WidgetOpenMode, ApplicationShell, PreferenceService } from '@theia/core/lib/browser';
import { EditorWidget, EditorOpenerOptions, EditorManager } from '@theia/editor/lib/browser';
import { EditorWidget, EditorOpenerOptions, EditorManager, CustomEditorWidget } from '@theia/editor/lib/browser';
import { MonacoEditor } from './monaco-editor';
import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
import { MonacoEditorModel } from './monaco-editor-model';

import ICodeEditor = monaco.editor.ICodeEditor;
import CommonCodeEditor = monaco.editor.CommonCodeEditor;
Expand Down Expand Up @@ -55,7 +56,13 @@ export class MonacoEditorService extends monaco.services.CodeEditorServiceImpl {
* Monaco active editor is either focused or last focused editor.
*/
getActiveCodeEditor(): monaco.editor.IStandaloneCodeEditor | undefined {
const editor = MonacoEditor.getCurrent(this.editors);
let editor = MonacoEditor.getCurrent(this.editors);
if (!editor && CustomEditorWidget.is(this.shell.activeWidget)) {
const model = this.shell.activeWidget.modelRef.object;
if (model.editorTextModel instanceof MonacoEditorModel) {
editor = MonacoEditor.findByDocument(this.editors, model.editorTextModel)[0];
}
}
return editor && editor.getControl();
}

Expand Down
8 changes: 6 additions & 2 deletions packages/monaco/src/browser/monaco-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,15 @@ import { injectable, inject, postConstruct } from 'inversify';
import URI from '@theia/core/lib/common/uri';
import { Emitter } from '@theia/core/lib/common/event';
import { FileSystemPreferences } from '@theia/filesystem/lib/browser';
import { EditorManager } from '@theia/editor/lib/browser';
import { CustomEditorWidget, EditorManager } from '@theia/editor/lib/browser';
import { MonacoTextModelService } from './monaco-text-model-service';
import { WillSaveMonacoModelEvent, MonacoEditorModel, MonacoModelContentChangedEvent } from './monaco-editor-model';
import { MonacoEditor } from './monaco-editor';
import { ProblemManager } from '@theia/markers/lib/browser';
import { MaybePromise } from '@theia/core/lib/common/types';
import { FileService } from '@theia/filesystem/lib/browser/file-service';
import { FileSystemProviderCapabilities } from '@theia/filesystem/lib/common/files';
import { ApplicationShell } from '@theia/core/lib/browser';

export namespace WorkspaceFileEdit {
export function is(arg: Edit): arg is monaco.languages.WorkspaceFileEdit {
Expand Down Expand Up @@ -99,6 +100,9 @@ export class MonacoWorkspace {
@inject(ProblemManager)
protected readonly problems: ProblemManager;

@inject(ApplicationShell)
protected readonly shell: ApplicationShell;

@postConstruct()
protected init(): void {
this.resolveReady();
Expand Down Expand Up @@ -162,7 +166,7 @@ export class MonacoWorkspace {
if (this.suppressedOpenIfDirty.indexOf(model) !== -1) {
return;
}
if (model.dirty && MonacoEditor.findByDocument(this.editorManager, model).length === 0) {
if (model.dirty && MonacoEditor.findByDocument(this.editorManager, model).length === 0 && !CustomEditorWidget.is(this.shell.activeWidget)) {
// create a new reference to make sure the model is not disposed before it is
// acquired by the editor, thus losing the changes that made it dirty.
this.textModelService.createModelReference(model.textEditorModel.uri).then(ref => {
Expand Down
39 changes: 38 additions & 1 deletion packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1424,6 +1424,40 @@ export interface WebviewsMain {
$unregisterSerializer(viewType: string): void;
}

export interface CustomEditorsExt {
$resolveWebviewEditor(
resource: UriComponents,
newWebviewHandle: string,
viewType: string,
title: string,
position: number,
options: theia.WebviewPanelOptions,
cancellation: CancellationToken): Promise<void>;
$createCustomDocument(resource: UriComponents, viewType: string, backupId: string | undefined, cancellation: CancellationToken): Promise<{ editable: boolean }>;
$disposeCustomDocument(resource: UriComponents, viewType: string): Promise<void>;
$undo(resource: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void>;
$redo(resource: UriComponents, viewType: string, editId: number, isDirty: boolean): Promise<void>;
$revert(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void>;
$disposeEdits(resourceComponents: UriComponents, viewType: string, editIds: number[]): void;
$onSave(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<void>;
$onSaveAs(resource: UriComponents, viewType: string, targetResource: UriComponents, cancellation: CancellationToken): Promise<void>;
// $backup(resource: UriComponents, viewType: string, cancellation: CancellationToken): Promise<string>;
$onMoveCustomEditor(handle: string, newResource: UriComponents, viewType: string): Promise<void>;
}

export interface CustomTextEditorCapabilities {
readonly supportsMove?: boolean;
}

export interface CustomEditorsMain {
$registerTextEditorProvider(viewType: string, options: theia.WebviewPanelOptions, capabilities: CustomTextEditorCapabilities): void;
$registerCustomEditorProvider(viewType: string, options: theia.WebviewPanelOptions, supportsMultipleEditorsPerDocument: boolean): void;
$unregisterEditorProvider(viewType: string): void;
$createCustomEditorPanel(handle: string, title: string, viewColumn: theia.ViewColumn | undefined, options: theia.WebviewPanelOptions & theia.WebviewOptions): Promise<void>;
$onDidEdit(resource: UriComponents, viewType: string, editId: number, label: string | undefined): void;
$onContentChange(resource: UriComponents, viewType: string): void;
}

export interface StorageMain {
$set(key: string, value: KeysToAnyValues, isGlobal: boolean): Promise<boolean>;
$get(key: string, isGlobal: boolean): Promise<KeysToAnyValues>;
Expand Down Expand Up @@ -1562,6 +1596,7 @@ export const PLUGIN_RPC_CONTEXT = {
LANGUAGES_MAIN: createProxyIdentifier<LanguagesMain>('LanguagesMain'),
CONNECTION_MAIN: createProxyIdentifier<ConnectionMain>('ConnectionMain'),
WEBVIEWS_MAIN: createProxyIdentifier<WebviewsMain>('WebviewsMain'),
CUSTOM_EDITORS_MAIN: createProxyIdentifier<CustomEditorsMain>('CustomEditorsMain'),
STORAGE_MAIN: createProxyIdentifier<StorageMain>('StorageMain'),
TASKS_MAIN: createProxyIdentifier<TasksMain>('TasksMain'),
DEBUG_MAIN: createProxyIdentifier<DebugMain>('DebugMain'),
Expand Down Expand Up @@ -1594,6 +1629,7 @@ export const MAIN_RPC_CONTEXT = {
LANGUAGES_EXT: createProxyIdentifier<LanguagesExt>('LanguagesExt'),
CONNECTION_EXT: createProxyIdentifier<ConnectionExt>('ConnectionExt'),
WEBVIEWS_EXT: createProxyIdentifier<WebviewsExt>('WebviewsExt'),
CUSTOM_EDITORS_EXT: createProxyIdentifier<CustomEditorsExt>('CustomEditorsExt'),
STORAGE_EXT: createProxyIdentifier<StorageExt>('StorageExt'),
TASKS_EXT: createProxyIdentifier<TasksExt>('TasksExt'),
DEBUG_EXT: createProxyIdentifier<DebugExt>('DebugExt'),
Expand All @@ -1604,7 +1640,8 @@ export const MAIN_RPC_CONTEXT = {
LABEL_SERVICE_EXT: createProxyIdentifier<LabelServiceExt>('LabelServiceExt'),
TIMELINE_EXT: createProxyIdentifier<TimelineExt>('TimeLineExt'),
THEMING_EXT: createProxyIdentifier<ThemingExt>('ThemingExt'),
COMMENTS_EXT: createProxyIdentifier<CommentsExt>('CommentsExt')};
COMMENTS_EXT: createProxyIdentifier<CommentsExt>('CommentsExt')
};

export interface TasksExt {
$provideTasks(handle: number, token?: CancellationToken): Promise<TaskDto[] | undefined>;
Expand Down
29 changes: 29 additions & 0 deletions packages/plugin-ext/src/common/plugin-protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export interface PluginPackageContribution {
configurationDefaults?: RecursivePartial<PreferenceSchemaProperties>;
languages?: PluginPackageLanguageContribution[];
grammars?: PluginPackageGrammarsContribution[];
customEditors?: PluginPackageCustomEditor[];
viewsContainers?: { [location: string]: PluginPackageViewContainer[] };
views?: { [location: string]: PluginPackageView[] };
viewsWelcome?: PluginPackageViewWelcome[];
Expand All @@ -89,6 +90,23 @@ export interface PluginPackageContribution {
resourceLabelFormatters?: ResourceLabelFormatter[];
}

export interface PluginPackageCustomEditor {
viewType: string;
displayName: string;
selector?: CustomEditorSelector[];
priority?: CustomEditorPriority;
}

export interface CustomEditorSelector {
readonly filenamePattern?: string;
}

export enum CustomEditorPriority {
default = 'default',
builtin = 'builtin',
option = 'option',
}

export interface PluginPackageViewContainer {
id: string;
title: string;
Expand Down Expand Up @@ -482,6 +500,7 @@ export interface PluginContribution {
configurationDefaults?: PreferenceSchemaProperties;
languages?: LanguageContribution[];
grammars?: GrammarsContribution[];
customEditors?: CustomEditor[];
viewsContainers?: { [location: string]: ViewContainer[] };
views?: { [location: string]: View[] };
viewsWelcome?: ViewWelcome[];
Expand Down Expand Up @@ -604,6 +623,16 @@ export interface FoldingRules {
markers?: FoldingMarkers;
}

/**
* Custom Editors contribution
*/
export interface CustomEditor {
viewType: string;
displayName: string;
selector: CustomEditorSelector[];
priority: CustomEditorPriority;
}

/**
* Views Containers contribution
*/
Expand Down
21 changes: 19 additions & 2 deletions packages/plugin-ext/src/hosted/browser/hosted-plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ import { FrontendApplicationConfigProvider } from '@theia/core/lib/browser/front
import { environment } from '@theia/application-package/lib/environment';
import { JsonSchemaStore } from '@theia/core/lib/browser/json-schema-store';
import { FileService, FileSystemProviderActivationEvent } from '@theia/filesystem/lib/browser/file-service';
import { PluginCustomEditorRegistry } from '../../main/browser/custom-editors/plugin-custom-editor-registry';
import { CustomEditorWidget } from '../../main/browser/custom-editors/custom-editor-widget';

export type PluginHost = 'frontend' | string;
export type DebugActivationEvent = 'onDebugResolve' | 'onDebugInitialConfigurations' | 'onDebugAdapterProtocolTracker';
Expand Down Expand Up @@ -151,6 +153,9 @@ export class HostedPluginSupport {
@inject(JsonSchemaStore)
protected readonly jsonSchemaStore: JsonSchemaStore;

@inject(PluginCustomEditorRegistry)
protected readonly customEditorRegistry: PluginCustomEditorRegistry;

private theiaReadyPromise: Promise<any>;

protected readonly managers = new Map<string, PluginManagerExt>();
Expand Down Expand Up @@ -197,9 +202,10 @@ export class HostedPluginSupport {
this.taskProviderRegistry.onWillProvideTaskProvider(event => this.ensureTaskActivation(event));
this.taskResolverRegistry.onWillProvideTaskResolver(event => this.ensureTaskActivation(event));
this.fileService.onWillActivateFileSystemProvider(event => this.ensureFileSystemActivation(event));
this.customEditorRegistry.onPendingOpenCustomEditor(event => this.activateByCustomEditor(event));

this.widgets.onDidCreateWidget(({ factoryId, widget }) => {
if (factoryId === WebviewWidget.FACTORY_ID && widget instanceof WebviewWidget) {
if ((factoryId === WebviewWidget.FACTORY_ID || factoryId === CustomEditorWidget.FACTORY_ID) && widget instanceof WebviewWidget) {
const storeState = widget.storeState.bind(widget);
const restoreState = widget.restoreState.bind(widget);

Expand Down Expand Up @@ -556,6 +562,10 @@ export class HostedPluginSupport {
await this.activateByEvent(`onCommand:${commandId}`);
}

async activateByCustomEditor(viewType: string): Promise<void> {
await this.activateByEvent(`onCustomEditor:${viewType}`);
}

activateByFileSystem(event: FileSystemProviderActivationEvent): Promise<void> {
return this.activateByEvent(`onFileSystem:${event.scheme}`);
}
Expand Down Expand Up @@ -713,10 +723,17 @@ export class HostedPluginSupport {
this.webviewRevivers.delete(viewType);
}

protected preserveWebviews(): void {
protected async preserveWebviews(): Promise<void> {
for (const webview of this.widgets.getWidgets(WebviewWidget.FACTORY_ID)) {
this.preserveWebview(webview as WebviewWidget);
}
for (const webview of this.widgets.getWidgets(CustomEditorWidget.FACTORY_ID)) {
(webview as CustomEditorWidget).modelRef.dispose();
if ((webview as any)['closeWithoutSaving']) {
delete (webview as any)['closeWithoutSaving'];
}
this.customEditorRegistry.resolveWidget(webview as CustomEditorWidget);
}
}

protected preserveWebview(webview: WebviewWidget): void {
Expand Down
Loading

0 comments on commit 3639693

Please sign in to comment.