Skip to content

Commit

Permalink
6636-custom-editor: support for CustomEditor API
Browse files Browse the repository at this point in the history
Signed-off-by: Dan Arad <dan.arad@sap.com>
  • Loading branch information
danarad05 committed Dec 29, 2020
1 parent f6d2085 commit 6be6df1
Show file tree
Hide file tree
Showing 33 changed files with 3,217 additions and 67 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 remove
*/
onOpenersStateChanged?: Event<void>;
}

export async function open(openerService: OpenerService, uri: URI, options?: OpenerOptions): Promise<object | undefined> {
Expand All @@ -85,11 +89,26 @@ export async function open(openerService: OpenerService, uri: URI, options?: Ope
@injectable()
export class DefaultOpenerService implements OpenerService {

protected readonly additionalHandlers: 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.additionalHandlers.push(openHandler);
this.onOpenersStateChangedEmitter.fire();

return Disposable.create(() => {
this.additionalHandlers.splice(this.additionalHandlers.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.additionalHandlers
];
}

}
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
7 changes: 7 additions & 0 deletions packages/monaco/src/browser/monaco-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,14 @@ export class MonacoEditorCommandHandlers implements CommandContribution {
);
},
isEnabled: () => {
/*
* We check monaco focused code editor first since they can contain inline like the debug console and embedded editors like in the peek reference.
* If there is not such then we check last focused editor tracked by us.
*/
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
10 changes: 9 additions & 1 deletion packages/monaco/src/browser/monaco-editor-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import { MonacoToProtocolConverter } from './monaco-to-protocol-converter';
import ICodeEditor = monaco.editor.ICodeEditor;
import CommonCodeEditor = monaco.editor.CommonCodeEditor;
import IResourceInput = monaco.editor.IResourceInput;
import { CustomEditorWidget } from './monaco-workspace';
import { MonacoEditorModel } from './monaco-editor-model';

decorate(injectable(), monaco.services.CodeEditorServiceImpl);

Expand Down Expand Up @@ -55,7 +57,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
18 changes: 17 additions & 1 deletion packages/monaco/src/browser/monaco-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ 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, Widget } from '@theia/core/lib/browser';
import { Reference } from '@theia/core/lib/common';

export namespace WorkspaceFileEdit {
export function is(arg: Edit): arg is monaco.languages.WorkspaceFileEdit {
Expand All @@ -48,6 +50,17 @@ export namespace WorkspaceTextEdit {
}
}

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>;
}

export type Edit = monaco.languages.WorkspaceFileEdit | monaco.languages.WorkspaceTextEdit;

export interface WorkspaceFoldersChangeEvent {
Expand Down Expand Up @@ -99,6 +112,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 +178,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
35 changes: 35 additions & 0 deletions packages/plugin-ext/src/common/plugin-api-rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ import type {
import { SerializableEnvironmentVariableCollection } from '@theia/terminal/lib/common/base-terminal-protocol';
import { ThemeType } from '@theia/core/lib/browser/theming';
import { Disposable } from '@theia/core/lib/common/disposable';
import { URI } from 'vscode-uri';

export interface PreferenceData {
[scope: number]: any;
Expand Down Expand Up @@ -1385,6 +1386,38 @@ export interface WebviewsMain {
$unregisterSerializer(viewType: string): void;
}

export interface CustomEditorsExt {
$resolveCustomEditorWebview(
handle: string,
viewType: string,
resource: URI,
title: string,
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>;
$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, 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 @@ -1494,6 +1527,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 @@ -1525,6 +1559,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 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 @@ -481,6 +499,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 @@ -603,6 +622,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
27 changes: 26 additions & 1 deletion packages/plugin-ext/src/hosted/node/scanners/scanner-theia.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,10 @@ import {
PluginCommand,
IconUrl,
ThemeContribution,
IconThemeContribution
IconThemeContribution,
PluginPackageCustomEditor,
CustomEditor,
CustomEditorPriority
} from '../../../common/plugin-protocol';
import * as fs from 'fs';
import * as path from 'path';
Expand Down Expand Up @@ -177,6 +180,15 @@ export class TheiaPluginScanner implements PluginScanner {
console.error(`Could not read '${rawPlugin.name}' contribution 'grammars'.`, rawPlugin.contributes!.grammars, err);
}

try {
if (rawPlugin.contributes!.customEditors) {
const customEditors = this.readCustomEditors(rawPlugin.contributes.customEditors!);
contributions.customEditors = customEditors;
}
} catch (err) {
console.error(`Could not read '${rawPlugin.name}' contribution 'customEditors'.`, rawPlugin.contributes!.customEditors, err);
}

try {
if (rawPlugin.contributes && rawPlugin.contributes.viewsContainers) {
const viewsContainers = rawPlugin.contributes.viewsContainers;
Expand Down Expand Up @@ -467,6 +479,19 @@ export class TheiaPluginScanner implements PluginScanner {
};
}

private readCustomEditors(rawCustomEditors: PluginPackageCustomEditor[]): CustomEditor[] {
return rawCustomEditors.map(rawCustomEditor => this.readCustomEditor(rawCustomEditor));
}

private readCustomEditor(rawCustomEditor: PluginPackageCustomEditor): CustomEditor {
return {
viewType: rawCustomEditor.viewType,
displayName: rawCustomEditor.displayName,
selector: rawCustomEditor.selector || [],
priority: rawCustomEditor.priority || CustomEditorPriority.default
};
}

private readViewsContainers(rawViewsContainers: PluginPackageViewContainer[], pck: PluginPackage): ViewContainer[] {
return rawViewsContainers.map(rawViewContainer => this.readViewContainer(rawViewContainer, pck));
}
Expand Down
Loading

0 comments on commit 6be6df1

Please sign in to comment.