Theia extension 常用开发代码

2,634 阅读4分钟

主要是 theia extension 开发过程中会遇到的代码

没有相关经验的同学,可以先了解下 InversifyJS, 再直接跑下官方提供的拓展例子,熟悉下基本的开发

命令(Command)

实现 CommandContribution 接口,如果传入的 Command 中带 label 字段的话,则该命令也会显示在命令面板中

@injectable()
export class TestCommandContribution implements CommandContribution {
    registerCommands(commands: CommandRegistry): void {
        commands.registerCommand(SayHello, {
            execute: () => {
                 this.messageService.info('hello hello');    // 提示框
            }
        });
    }
}

// fontend-module.ts
bind(CommandContribution).to(LdpCommandContribution).inSingletonScope();

1.gif

菜单(Menu)

实现 MenuContribution 接口,点击则会执行 Command.id 对应的命令

const SayHello: Command = {
    id: 'say:hello',    // 对应的 command id
    label: 'Say Hello',    // 菜单上显示的文字
    category: 'test'   // 类别
};

@injectable()
export class TestMenuContribution implements MenuContribution {
    registerMenus(menus: MenuModelRegistry): void {
        menus.registerMenuAction(CommonMenus.FILE, {
            commandId: SayHello.id,
            label: SayHello.label,
        });
    }
}

// fontend-module.ts
bind(MenuContribution).to(LdpMenuContribution).inRequestScope();

image.png

自定义视图

  • 实现 BaseWidgetReactWidget 接口,自定义视图
import * as React from 'react';
import { injectable, postConstruct, inject } from 'inversify';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';

@injectable()
export class WidgetWidget extends ReactWidget{
    static readonly ID = 'test:widget';
    static readonly LABEL = 'Test Widget';
    static readonly TOGGLE_COMMADND_ID = 'test.widget';

    @postConstruct()
    protected async init(): Promise<void> {
        // 初始化
        this.id = WidgetWidget.ID;
        this.title.label = WidgetWidget.LABEL;
        this.title.caption = WidgetWidget.LABEL;
        this.title.closable = true;
        this.title.iconClass = 'fa fa-window-maximize'; // example widget icon.
        this.update();    // 更新视图
    }

    protected render(): React.ReactNode {
        return (
            <div>自定义视图</div>
        )
    }
}
  • 继承抽象类 AbstractViewContribution,将视图添加到对应的 DOM

这个类中除了实现了 widget 绑定方法外,还包括命令、菜单、快捷键等方法的绑定

import { injectable, inject } from 'inversify';
import { Command, CommandRegistry, MenuModelRegistry, MessageService } from '@theia/core';
import { WidgetWidget } from './widget-widget';
import { AbstractViewContribution, CommonMenus } from '@theia/core/lib/browser';

const TestCommand: Command = {
    id: 'test',
    label: 'test'
}

@injectable()
export class WidgetContribution extends AbstractViewContribution<WidgetWidget> {=
    constructor() {
        super({
            widgetId: WidgetWidget.ID,
            widgetName: WidgetWidget.LABEL,
            defaultWidgetOptions: { area: 'main'},
            toggleCommandId: WidgetWidget.ID
        });
    }

    // 通过改写 registerCommands 方法,绑定其他命令
    registerCommands(commands: CommandRegistry) {
        super.registerCommands(commands);

        commands.registerCommand(TestCommand, {
            execute: () => console.log(123)
        })
    }

    // 通过改写 registerMenus 方法,绑定其他菜单项
    registerMenus(menus: MenuModelRegistry) {
        super.registerMenus(menus);

        menus.registerMenuAction(CommonMenus.VIEW_VIEWS, {
            commandId: TestCommand.id,
            label: TestCommand.label
        })
    }
}
  • 绑定到容器中
import { ContainerModule } from 'inversify';
import { WidgetWidget } from './widget-widget';
import { WidgetContribution } from './widget-contribution';
import { bindViewContribution, FrontendApplicationContribution, WidgetFactory } from '@theia/core/lib/browser';

export default new ContainerModule(bind => {
    bindViewContribution(bind, WidgetContribution);
    bind(FrontendApplicationContribution).toService(WidgetContribution);
    bind(WidgetWidget).toSelf();
    bind(WidgetFactory).toDynamicValue(ctx => ({
        id: WidgetWidget.ID,
        createWidget: () => ctx.container.get<WidgetWidget>(WidgetWidget)
    })).inSingletonScope();
});

1.gif

自定义编辑器显示

也就是拦截 Theia 的默认编辑器

  • 实现 OpenHandler 接口

WidgetOpenHandler 实际也是实现了 OpenerHandler 接口,当我们有其他定制化需求的时候,就可以自己实现 OpenHandler 接口,具体可以仿照 WidgetOpenHandler 实现,不展开说明

  • 继承抽象类 WidgetOpenHandler
// widget.ts
import * as React from 'react';
import { injectable, postConstruct } from 'inversify';
import { ReactWidget } from '@theia/core/lib/browser/widgets/react-widget';

@injectable()
export class CustomWidget extends ReactWidget {
    static readonly ID = 'test:widget';
    static readonly LABEL = 'Custom Editor';

    protected text: string;

    @postConstruct()
    protected async init(): Promise<void> {
        // 初始化
        this.id = WidgetWidget.ID;
        this.title.label = WidgetWidget.LABEL;
        this.title.caption = WidgetWidget.LABEL;
        this.title.closable = true;
        this.title.iconClass = 'fa fa-window-maximize'; // example widget icon.
        this.update();    // 更新视图
    }

    setText(text: string) {
        this.text = text;
    }

    // 根据接收到的参数显示
    protected render(): React.ReactNode {
        return (
            <React.Fragment>
                <div>自定义编辑器</div>
                <div>{this.text}</div>
            </React.Fragment>
        )
    }
}
// 继承 WidgetOpenerHandler
import { injectable } from 'inversify';
import { WidgetOpenHandler, WidgetOpenerOptions } from '@theia/core/lib/browser';
import { CustomWidget } from './widget-widget';
import URI from '@theia/core/lib/common/uri';

export interface CustomWidgetOptions {
    text: string;
}

@injectable()
export class CustomOpenHandler extends WidgetOpenHandler<CustomWidget>  {
    readonly id = CustomWidget.ID;
    
    canHandle(uri: URI): number {
        console.log(uri.path.ext);
        if(uri.path.ext === '.json') {
            return 500;
        }
        return 0;
    }

    // 这里可以设置传递给 widget 的参数
    createWidgetOptions(uri: URI, options?: WidgetOpenerOptions): CustomWidgetOptions{
        return { 
            text: '这是 json 文件'
        };
    }
}
// fontend-module.ts
bind(OpenHandler).toService(CustomOpenHandler);
bind(CustomOpenHandler).toSelf().inSingletonScope();    
bind(CustomWidget).toSelf();
bind(WidgetFactory).toDynamicValue(ctx => ({
    id: CustomWidget.ID,
    createWidget: (options: CustomWidgetOptions) => {
        const widget = ctx.container.get<CustomWidget>(CustomWidget);
        console.log(options);
        widget.setText(options.text);
        return widget;
    }
})).inSingletonScope();

当打开 json 文件的时候,就会显示我们自定义的编辑器

image.png

弹窗

框架自带

MessageService(右下角通知提示框)

@inject(MessageService)
protected messageService: MessageService;

this.messageService.info('hello hello');

image.png

对话框

commands.registerCommand(DialogCommand, {
    execute: async () => {
        const confirmed = await new ConfirmDialog({
            title: '这是个确认框',
            msg: '确认执行吗?',
            ok: '确认',
            cancel: '取消'
        }).open();

        console.log('确认了吗', confirmed);
    }
});

1.gif

框架还提供了 SingleTextInputDialog 等其他对话框,具体可看 packages/core/src/browser/dialogs.ts 这个目录

FileDialogService(文件/目录选择框)

  • showOpenDialog:选择文件/目录,返回选择的 URI
commands.registerCommand(FileDialog, {
    execute: async () => {
       const uri = await this.fileDialogService.showOpenDialog({
           title: '选择目录',
           canSelectFiles: false,
           canSelectFolders: true,
           openLabel: '选择',
       });

       console.log('选择路径', uri);
    }
});

1.gif

  • showSaveDialog:保存文件对话框
commands.registerCommand(FileDialog, {
    execute: async () => {
       const uri = await this.fileDialogService.showSaveDialog({
           title: '选择保存目录',
           saveLabel: '保存'
       });

       console.log('保存路径', uri);
    }
});

1.gif

自定义对话框

实现 AbstractDialogReactDialog 接口 (具体可参考 packages/core/src/browser/dialogs.ts 其他 Dialog 的实现)

// customDialog.tsx
import * as React from 'react';
import { inject, injectable } from 'inversify';
import { ReactDialog } from '@theia/core/lib/browser/dialogs/react-dialog';
import { DialogProps } from '@theia/core/lib/browser/dialogs';

// 定义入参
@injectable()
export class CustomDialogProps extends DialogProps {
    readonly text: string;
    readonly okValue: string;
    readonly cancelValue: string;
}

// 定义返回
interface CustomDialogValue {
    text: string;
}

@injectable()
export class CustomDialog extends ReactDialog<CustomDialogValue> {
    protected readonly text: string;

    constructor(
        @inject(CustomDialogProps) protected readonly props: CustomDialogProps
    ) {
        super(props);
        const { text, okValue, cancelValue } = this.props;
        this.text = text;
        this.appendCloseButton(cancelValue);
        this.appendAcceptButton(okValue);
    }

    protected render(): React.ReactNode {
        return (
            <div>
                {this.text}
            </div>
        );
    }

    get value(): CustomDialogValue {
        return {
            text: this.text
        }
    }
}
// fontend-module.ts
bind(CustomDialogProps).toSelf();
bind(CustomDialog).toSelf();
commands.registerCommand(SayHello, {
    execute: async () => {
       const text = await new CustomDialog({
           title: '测试对话框',
           text: '测试',
           okValue: '保存',
           cancelValue: '取消'
       }).open();

        console.log('返回文字', text);
    }
});

1.gif

自定义编辑器的实现

除了上述 openHandler 的实现外,我们还需要实现编辑器相关的其他功能

github.com/eclipsesour…

文件处理 API

  • FlieStat
  • URI

TreeWidget

Preference

持续更新中