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

6636-custom-editor: support for CustomEditor API #8910

Merged
merged 1 commit into from
Mar 7, 2021
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: 8 additions & 1 deletion packages/core/src/browser/opener-service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import { DefaultOpenerService, OpenHandler } from './opener-service';
import * as assert from 'assert';
import { MaybePromise } from '../common/types';
import * as chai from 'chai';
const expect = chai.expect;

const id = 'my-opener';
const openHandler: OpenHandler = {
Expand All @@ -34,9 +36,14 @@ const openerService = new DefaultOpenerService({
});

describe('opener-service', () => {

it('getOpeners', () =>
openerService.getOpeners().then(openers => {
assert.deepStrictEqual([openHandler], openers);
}));
it('addHandler', () => {
openerService.addHandler(openHandler);
openerService.getOpeners().then(openers => {
expect(openers.length).is.equal(2);
});
});
});
30 changes: 28 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,14 @@ export interface OpenerService {
* Reject if such does not exist.
*/
getOpener(uri: URI, options?: OpenerOptions): Promise<OpenHandler>;
/**
* Add open handler i.e. for custom editors
*/
addHandler?(openHandler: OpenHandler): Disposable;
/**
* Event that fires when a new opener is added or removed.
*/
onDidChangeOpeners?: Event<void>;
}

export async function open(openerService: OpenerService, uri: URI, options?: OpenerOptions): Promise<object | undefined> {
Expand All @@ -84,12 +92,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 onDidChangeOpenersEmitter = new Emitter<void>();
readonly onDidChangeOpeners = this.onDidChangeOpenersEmitter.event;

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

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

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

async getOpener(uri: URI, options?: OpenerOptions): Promise<OpenHandler> {
const handlers = await this.prioritize(uri, options);
if (handlers.length >= 1) {
Expand All @@ -114,7 +137,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
1 change: 1 addition & 0 deletions packages/monaco/src/browser/monaco-editor-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export class MonacoEditorModel implements ITextEditorModel, TextEditorDocument {

autoSave: 'on' | 'off' = 'on';
autoSaveDelay: number = 500;
suppressOpenEditorWhenDirty = false;
danarad05 marked this conversation as resolved.
Show resolved Hide resolved
/* @deprecated there is no general save timeout, each participant should introduce a sensible timeout */
readonly onWillSaveLoopTimeOut = 1500;
protected bufferSavedVersionId: number;
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];
danarad05 marked this conversation as resolved.
Show resolved Hide resolved
}
}
return editor && editor.getControl();
}

Expand Down
2 changes: 1 addition & 1 deletion packages/monaco/src/browser/monaco-workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ export class MonacoWorkspace {
protected readonly suppressedOpenIfDirty: MonacoEditorModel[] = [];

protected openEditorIfDirty(model: MonacoEditorModel): void {
if (this.suppressedOpenIfDirty.indexOf(model) !== -1) {
if (model.suppressOpenEditorWhenDirty || this.suppressedOpenIfDirty.indexOf(model) !== -1) {
danarad05 marked this conversation as resolved.
Show resolved Hide resolved
return;
}
if (model.dirty && MonacoEditor.findByDocument(this.editorManager, model).length === 0) {
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 @@ -1440,6 +1440,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>;
paul-marechal marked this conversation as resolved.
Show resolved Hide resolved
$onMoveCustomEditor(handle: string, newResource: UriComponents, viewType: string): Promise<void>;
}
danarad05 marked this conversation as resolved.
Show resolved Hide resolved

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 @@ -1578,6 +1612,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 @@ -1610,6 +1645,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 @@ -1620,7 +1656,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 @@ -90,6 +91,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 @@ -489,6 +507,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 @@ -612,6 +631,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.onWillOpenCustomEditor(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