Theia 目前提供的开发文档中对于 API 的介绍不太详细,缺少可以直接执行的示例,新手在新功能开发中不太容易理解,本文将阅读源码过程的一些代码片段摘出来进行归纳总结,通过局部的代码片段窥探基于 Theia 如何定制 IDE。
前端:
import { WorkspaceService } from '@theia/workspace/lib/browser';
private getCurrentWorkspaceUri(): URI | undefined {
return this.workspaceService.workspace?.resource;
}
后端:
import { WorkspaceServer } from '@theia/workspace/lib/common';
private getProjectPath(): string | undefined {
const projectPath = await this.workspaceServer.getMostRecentlyUsedWorkspace();
if (projectPath) {
return new URI(projectPath).path.toString();
}
}
类似于 Electron showOpenDialog,自动根据运行环境切换。
import { FileDialogService } from '@theia/filesystem/lib/browser';
@inject(FileDialogService)
protected readonly fileDialogService: FileDialogService;
const uri = await this.fileDialogService.showOpenDialog({
title: '标题',
canSelectFiles: true,
canSelectFolders: false
});
前端拓展获取服务接口信息:
import { Endpoint } from '@theia/core/lib/browser/endpoint';
// 获取 http://localhost:{port}/files
new Endpoint({ path: 'files' }).getRestUrl().toString();
Tips: IDE 前端启动过程中获取到后端服务 address 信息后,将端口设置在 location.search 上。
后端拓展获取端口信息:
@injectable()
export class SimulatorEndpoint implements BackendApplicationContribution {
onStart(server: http.Server | https.Server) {
console.log('address: ', server.address());
}
}
this.shell.onDidAddWidget((widget: Widget) => {
console.log('add widget: ', widget.id);
});
this.shell.onDidRemoveWidget((widget: Widget) => {
console.log('remove widget: ', widget.id);
});
settings.json
, keymaps.json
, recentworkspace.json
等配置文件的根目录默认地址是 ~/.theia,可以通过复写 EnvVariablesServer API 的 getConfigDirUri 方法进行自定义,最简单的方法是继承EnvVariablesServerImpl 子类,然后将其重新绑定到 backend 模块:
// your-env-variables-server.ts:
import { injectable } from 'inversify';
import { EnvVariablesServerImpl } from '@theia/core/lib/node/env-variables';
@injectable()
export class YourEnvVariableServer extends EnvVariablesServerImpl {
async getConfigDirUri(): Promise<string> {
return 'file:///path/to/your/desired/config/dir';
}
}
// your-backend-application-module.ts:
import { ContainerModule } from 'inversify';
import { EnvVariablesServer } from '@theia/core/lib/common/env-variables';
import { YourEnvVariableServer } from './your-env-variables-server';
export default new ContainerModule((bind, unbind, isBound, rebind) => {
rebind(EnvVariablesServer).to(YourEnvVariableServer).inSingletonScope();
});
v1.5.0 有更加简单的配置方式,通过配置 process.env.THEIA_CONFIG_DIR 字段。
前端工程的配置修改方式:
rebind(PreferenceConfigurations).to(class extends PreferenceConfigurations {
getPaths(): string[] {
return [CUSTOM_CONFIG_DIR, '.theia', '.vscode'];
}
}).inSingletonScope();
Theia 自定义编辑器视图比 VS Code 更简单,有两种实现方式:
module 注册服务:
bind(ProjectConfigWidget).toSelf();
bind(WidgetFactory).toDynamicValue(ctx => ({
id: ProjectConfigWidget.ID,
createWidget: () => ctx.container.get<ProjectConfigWidget>(ProjectConfigWidget)
})).inSingletonScope();
bind(ProjectConfigOpenHandler).toSelf().inSingletonScope();
bind(OpenHandler).toService(ProjectConfigOpenHandler);
// project-config-open-handler.ts
@injectable()
export class ProjectConfigOpenHandler extends WidgetOpenHandler<ProjectConfigWidget> {
readonly id = ProjectConfigWidget.ID;
readonly label?: string = 'Preview';
private defaultFileName: string = 'project.config.json';
@inject(ApplicationShell)
protected readonly shell: ApplicationShell;
protected createWidgetOptions(uri: URI): Object {
return {};
}
canHandle(uri: URI): number {
if (uri.path.toString().includes(this.defaultFileName)) {
return 1000;
}
return 0;
}
}
import { ApplicationShellMouseTracker } from '@theia/core/lib/browser/shell/application-shell-mouse-tracker';
@inject(ApplicationShellMouseTracker)
protected readonly mouseTracker: ApplicationShellMouseTracker;
// Style from core
const TRANSPARENT_OVERLAY_STYLE = 'theia-transparent-overlay';
@postConstruct()
protected init(): void {
this.frame = this.createWebView();
this.transparentOverlay = this.createTransparentOverlay();
this.node.appendChild(this.frame);
this.node.appendChild(this.transparentOverlay);
this.toDispose.push(this.mouseTracker.onMousedown(e => {
if (this.frame.style.display !== 'none') {
this.transparentOverlay.style.display = 'block';
}
}));
this.toDispose.push(this.mouseTracker.onMouseup(e => {
if (this.frame.style.display !== 'none') {
this.transparentOverlay.style.display = 'none';
}
}));
}
createWebView(): Electron.WebviewTag {
const webview = document.createElement('webview') as Electron.WebviewTag;
...
return webview;
}
createTransparentOverlay() {
const transparentOverlay = document.createElement('div');
transparentOverlay.classList.add(TRANSPARENT_OVERLAY_STYLE);
transparentOverlay.style.display = 'none';
return transparentOverlay;
}
import { EditorManager, EditorWidget } from '@theia/editor/lib/browser';
@inject(EditorManager)
protected readonly editorManager: EditorManager;
/**
* 检查编辑器是否可以保存
*/
checkEditorSaveable() {
let isDirty = false;
const trackedEditors: EditorWidget[] = this.editorManager.all;
for (let widget of trackedEditors) {
isDirty = widget.saveable.dirty;
if (isDirty) break;
}
return isDirty;
}