前言

VS Code 之所以是最流行的开发者工具,与其强大的插件生态是分不开的,VS Code 生态内有各种增强功能的 VS Code Extensions,Theia 在 VS Code 拓展机制上又进一步设计,增加基于 Extension 和

Plug-ins 两种不同的拓展方法,本文将对 Theia 插件拓展机制进行详细说明,在理解 Theia 的拓展机制之前,会先介绍 VS Code 拓展的一些基础作为铺垫,从而更容易理解 Theia 为啥有更强大的拓展能力。

VS Code Extensions

Visual Studio Code 在设计上充分考虑了可扩展性,从 UI 到编辑体验,几乎可以通过扩展 API 对 VS Code 的每个部分进行自定义和增强。实际上,VS Code 的许多核心功能都是作为扩展构建的,并使用相同的扩展API。

拓展的开放能力

从 VS Code 官网 Extensions Capabilities Overview 文档中我们可以看到,VS Code 拓展的能力建立在Contribution Points和 VS Code API 之上,这些能力分为六大类:通用能力、主题、声明性语言功能、程序语言功能、工作区拓展、调试。

通用功能:可以在任何扩展中使用的核心功能。

  • 注册命令,配置,键绑定或上下文菜单项
  • 存储工作区或全局数据
  • 显示通知消息
  • 使用快速选择收集用户输入
  • 打开系统文件选择器,让用户选择文件或文件夹
  • 使用 Progress API 指示长时间运行的操作

主题主题化控制VS Code的外观,包括编辑器中源代码的颜色和VS Code UI的颜色。

  • 更改源代码的颜色
  • 更改 VS Code 用户界面的颜色
  • 将现有的 TextMate 主题移植到 VS Code
  • 添加自定义文件图标

声明性语言功能:声明性语言功能增加了对编程语言的基本文本编辑支持,例如括号匹配,自动缩进和语法突出显示。 这是声明式完成的,无需编写任何代码。

  • 常用 JavaScript 代码片段
  • 提供新编程语言信息
  • 添加或替换编程语言的语法
  • 通过语法注入扩展现有语法
  • 将现有的 TextMate 语法移植到 VS Code

程序语言功能:编程语言功能添加了丰富的编程语言支持,例如悬停,转到定义,诊断错误,IntelliSense 和CodeLens,这些语言功能通过公开 vscode.languages.* API,扩展可以直接使用这些 API,也可以编写语言服务器,并使用 VS Code 语言服务器库适配。

  • 添加悬停显示 API 的用法示例
  • 使用诊断报告源代码中的拼写错误或 linter 错误
  • 注册 HTML 的新代码格式化程序
  • 提供丰富的上下文感知 IntelliSense
  • 添加对语言的折叠,面包屑和轮廓支持

工作区拓展:在 VS Code Workbench 界面中添加自定义组件和视图。

  • 将自定义上下文菜单操作添加到文件资源管理器
  • 在侧边栏中创建一个新的交互式 TreeView
  • 定义一个新的活动栏视图
  • 在状态栏中显示新信息
  • 使用 WebView API 呈现自定义内容
  • 贡献源代码控制提供程序

调试:通过编写将 VS Code 的调试 UI 连接到特定调试器或运行时的 Debugger Extensions,支持调试特定的运行时。

  • 通过 Debug Adapter implementation 将 VS Code 的调试 UI 连接到调试器或运行时
  • 指定调试器扩展支持的语言
  • 为调试器使用的调试配置属性提供丰富的 IntelliSense 和悬停信息
  • 提供调试配置摘要

VS Code 还提供了一组 Debug Extension API,可以使用它在任何 VS Code 调试器之上实现与调试相关的功能。

  • 基于动态创建的调试配置启动调试会话
  • 跟踪调试会话的生命周期
  • 以编程方式创建和管理断点

为了确保 VS Code 的稳定性和性能,还对扩展进行了限制,例如,扩展不能访问 VS Code UI 的 DOM。

拓展的生命周期

VS Code 插件的形态和一个 npm 包非常相似,需要在项目的根目录添加 package.json,并且在其中增加一些 VS Code 的配置,其中最主要的设置是 Activation Events(插件的激活时机) 和 contribution points (插件的能力)。

一个 VS Code 的生命周期中有两个钩子函数,activate 函数和 deactivate 函数,这两个函数需要在插件 npm 模块的入口文件 export 出去给 VS Code 加载并解析插件时调用。

其中,activate 会在 vscode 认为合适的时机调用,并且在插件的运行周期内只调用一次,因此在 activate 函数中开始启动插件的逻辑是一个非常合适的时机。deactivate 函数会在插件卸载之前调用,如果你的卸载逻辑中存在异步操作,那么只需要在 deactivate 函数中 retuen 一个 Promise 对象,VS Code 会在 Promise resolve 时才正式将插件卸载掉。

可以看到在 activate 函数执行之前,还有 onLanguage 等事件的描述,实际上这些就是声明在插件 package.json 文件中的 Activation Events。声明这些 Activation Events 后,VS Code 就会在适当的时机回调插件中的 activate 函数。VS Code 之所以这么设计,是为了节省资源开销,只在必要的时候才激活你的插件。当然如果你的插件非常重要,不希望在某个事件之后才被激活,你可以声明 Activation Events 为 * 这样 VS Code 就会在启动的时候就开始回调 activate 函数。

拓展的 Contribution Points

Contribution Points 是在 package.json 扩展清单的 contributes 字段中进行的一组 JSON 声明,扩展注册了贡献点,以扩展 Visual Studio Code 中的各种功能。以下是所有可用贡献点的列表:

configuration 用户设置/工作区设置的配置 configurationDefaults 默认编辑器配置
commands 命令 menus 菜单选项
keybindings 快捷键 languages 编程语言支持
debuggers 调试器 breakpoints 断点
grammars 语法高亮 themes 主题
iconThemes 文件图标主题 productIconThemes 产品图标主题
snippets 代码片段 jsonValidation JSON 配置文件校验
views 自定义视图 viewsContainers 自定义视图容器
problemMatchers 问题匹配器模式 problemPatterns 命名问题模式
taskDefinitions 定义任务 colors 定义颜色名称
typescriptServerPlugins TypeScript 服务器插件 resourceLabelFormatters 申明 URL Scheme

Theia Extensions

Theia Extension 入门示例

在深入探讨技术细节之前,让我们从概念上了解扩展在 Theia 中的工作方式。Theia 应用程序由 Extensions 组成。 Extensions 为特定功能提供了一组 Widgets,Commands,Handlers 等。 Theia 本身提供了许多扩展程序,例如 editors,terminals,project等。要创建基于 Theia 的可执行工具或 IDE,需选择和开发需要的拓展。

自定义的 IDE 中的拓展可以是独立方式添加功能,也可以是以松耦合的方式依赖另外一个拓展实现功能,Theia使用 Inversify.js 依赖项注入上下文,通过 DI 容器处理拓展之间的依赖关系。简而言之,这就像一个全局公告板,拓展可以去修改上面的内容。

每个 Extension 都是通过 npm 包的形式提供给 Theia Application 集成。VS Code 中 Contribution Points 比较局限,只能做一些基础的配置及使用特定 API 进行有限操作,而 Theia 中定义了大量的 contribution 接口,通过实现 *Contribution 类型的接口扩展可以为应用增加很多功能。

Theia Extension 主要有三部分的内容:

  • package.json 中常规配置及 Theia 特定的配置;
  • 与 Theia Framework 及其他 Extension 关联的 DI 模块;
  • 自定义 Extension 的实现。

在 Theia 中一个 Extension 定义了一个或者多个 DI 模块,需要在 Extension 的 package.json 的 theiaExtensions 字段中,例如 monaco 模块的配置:

{"keywords": ["theia-extension"],..."theiaExtensions": [{"frontend": "lib/browser/monaco-browser-module","frontendElectron": "lib/electron-browser/monaco-electron-module"}]
}

第一个是关键字 "theia-extension",将 npm 包标记为 Theia 扩展,Theia 将在启动时遍历所有的 npm 包,并处理标记为扩展包。theiaExtensions 字段枚举拓展的模块,每个模块都是拓展程序的入口,模块将扩展与 Theia提供的现有扩展(即平台)连接起来。根据代码的运行环境,总共有四种类型的 DI 模块。

export interface Extension {frontend?: string;frontendElectron?: string;backend?: string;backendElectron?: string;
}

根据 《Authoring Theia Extensions》的文档创建 Extension 工程。

npm install -g yo generator-theia-extension
mkdir theia-hello-world-extension
cd theia-hello-world-extension
yo theia-extension # select the 'Hello World' option and complete the prompts

我们可以看到 theia-hello-world-extension 插件是一个独立的 npm 包,package.json 文件通过 theiaExtensions 字段注册模块。

{"name": "theia-hello-world-extension",..."theiaExtensions": [{"frontend": "lib/browser/theia-hello-world-extension-frontend-module"}]
}

theia-hello-world-extension-frontend-module.ts 实现如下:

import { TheiaHelloWorldExtensionCommandContribution, TheiaHelloWorldExtensionMenuContribution } from './theia-hello-world-extension-contribution';
import {CommandContribution,MenuContribution
} from "@theia/core/lib/common";import { ContainerModule } from "inversify";export default new ContainerModule(bind => {// add your contribution bindings herebind(CommandContribution).to(TheiaHelloWorldExtensionCommandContribution);bind(MenuContribution).to(TheiaHelloWorldExtensionMenuContribution);
});

在 ContainerModule 中绑定 CommandContribution(命令扩展点)、MenuContribution(菜单扩展点) 类型的扩展点。CommandContribution 接口定义了 registerCommands 方法,MenuContribution 接口定义了 registerMenus 方法。自定义 Extension 具体实现如下:

import { injectable, inject } from "inversify";
import { CommandContribution, CommandRegistry, MenuContribution, MenuModelRegistry, MessageService } from "@theia/core/lib/common";
import { CommonMenus } from "@theia/core/lib/browser";export const TheiaHelloWorldExtensionCommand = {id: 'TheiaHelloWorldExtension.command',label: "Say Hello"
};@injectable()
export class TheiaHelloWorldExtensionCommandContribution implements CommandContribution {constructor(@inject(MessageService) private readonly messageService: MessageService,) { }registerCommands(registry: CommandRegistry): void {registry.registerCommand(TheiaHelloWorldExtensionCommand, {execute: () => this.messageService.info('Hello, world!')});}
}@injectable()
export class TheiaHelloWorldExtensionMenuContribution implements MenuContribution {registerMenus(menus: MenuModelRegistry): void {menus.registerMenuAction(CommonMenus.EDIT_FIND, {commandId: TheiaHelloWorldExtensionCommand.id,label: TheiaHelloWorldExtensionCommand.label});}
}

CommandContribution 图示如下:

MenuContribution 图示如下:

这个 Hello World 例子展示了如何通过 CommandRegistry 的 registerCommand 方法注册一个 Command,同时将通过菜单项关联 Command。我们也可以通过 API 方式调用 Command:

this.commandService.executeCommand<T>(command: string, ...args: any[]): Promise<T | undefined>;

Theia Extension 运行流程

上面的 Hello World 例子展示了如何写一个最简单的拓展,下面我们就根据上面的例子说明一下整体的运行机制。这里我们以 Menu 的拓展为例子加以说明,Menu 整体的交互逻辑如下:

MenuContribution 实现如下:

/*** Representation of a menu contribution.*/
export interface MenuContribution {/*** Registers menus.* @param menus the menu model registry.*/registerMenus(menus: MenuModelRegistry): void;
}@injectable()
export class MenuModelRegistry {protected readonly root = new CompositeMenuNode('');constructor(@inject(ContributionProvider) @named(MenuContribution)protected readonly contributions: ContributionProvider<MenuContribution>,@inject(CommandRegistry) protected readonly commands: CommandRegistry) { }onStart(): void {for (const contrib of this.contributions.getContributions()) {contrib.registerMenus(this);}}registerMenuAction(menuPath: MenuPath, item: MenuAction): Disposable {const parent = this.findGroup(menuPath);const actionNode = new ActionMenuNode(item, this.commands);return parent.addNode(actionNode);}...getMenu(menuPath: MenuPath = []): CompositeMenuNode {return this.findGroup(menuPath);}
}

MenuContribution 是一个接口,MenuModelRegistry onStart 方法调用所有的拓展实例的 registerMenus 方法,从而实现 Menu 的注册。MenuModelRegistry 在 Browser 和 Electron 环境中各自去实现。以 Browser 环境为例,MenuModelRegistry 在 @theia/core 模块的 src/browser/menu/browser-menu-plugin.ts 的 BrowserMainMenuFactory 类中被调用,BrowserMainMenuFactory 注入在 BrowserMenuBarContribution 中,BrowserMenuBarContribution 实现了 FrontendApplicationContribution,从而在 FrontendApplication 启动的过程被挂载。

@injectable()
export class BrowserMenuBarContribution implements FrontendApplicationContribution {@inject(ApplicationShell)protected readonly shell: ApplicationShell;constructor(@inject(BrowserMainMenuFactory) protected readonly factory: BrowserMainMenuFactory) { }onStart(app: FrontendApplication): void {const logo = this.createLogo();app.shell.addWidget(logo, { area: 'top' });const menu = this.factory.createMenuBar();app.shell.addWidget(menu, { area: 'top' });}...
}

FrontendApplicationContribution 接口的 onStart 方法,可以在应用的前端启动时增加自定义视图。参考 Menu 的实现,我们通过实现 FrontendApplicationContribution 可以不需要修改 Theia 框架源码很方便的拓展定制 Theia Application 的 UI,而这在 VS Code 中不改源码是没法实现的。

Theia Extension 内部通信

上面我们一个简单的拓展介绍了 Extension 如何注册命令,Extension 根据代码依赖的执行环境不同可以分为 common、browser、node、electron-node、electron-browser、electron-main。根据模块最终运行的环境可以分为:frontend、frontendElectron、backend、backendElectron、electronMain 五类类。代码运行环境差异导致不能简单的调用,在很多业务场景下我们需要实现逻辑交互。从上面的例子知道,Extension 之间可以通过 Command 的方式进行调用,那么 Extension 内部的 Frontend 和 Backend 怎么实现代码调用呢?

Frontend 调用 Backend 模块的方法的步骤为:

  • 1.定义 Backend Service 对外暴露的接口
// common/protocol.ts
export const HELLO_BACKEND_PATH = '/services/helloBackend';
// 变量声明
export const HelloBackendService = Symbol('HelloBackendService');
// 类型声明
export interface HelloBackendService {sayHelloTo(name: string): Promise<string>
}

这里需要注意的是 HelloBackendService 在这里两个含义是不一样的,变量声明用于表示 Service 的服务标识符(serviceIdentifier),类型声明是用于表示 Service 定义的接口。

  • 2.实现并注册 Backend Service 实例
// node/hello-backend-service.ts
import { injectable } from "inversify";
import { HelloBackendService } from "../common/protocol";@injectable()
export class HelloBackendServiceImpl implements HelloBackendService {sayHelloTo(name: string): Promise<string> {return new Promise<string>(resolve => resolve('Hello ' + name));}
}// node/theia-backend-communication-extension-backend-module.ts
bind(HelloBackendService).to(HelloBackendServiceImpl).inSingletonScope()
bind(ConnectionHandler).toDynamicValue(ctx =>new JsonRpcConnectionHandler(HELLO_BACKEND_PATH, () => {return ctx.container.get<HelloBackendService>(HelloBackendService);})
).inSingletonScope();
  • 3.Frontend 需要注册 Backend Service 接口同名的模块
// browser/theia-backend-communication-extension-frontend-module.ts
bind(HelloBackendService).toDynamicValue(ctx => {const connection = ctx.container.get(WebSocketConnectionProvider);return connection.createProxy<HelloBackendService>(HELLO_BACKEND_PATH);
}).inSingletonScope();
  • 4.前端调用 Backend Service 模块的方法
@inject(HelloBackendService)
private readonly helloBackendService: HelloBackendService;// 调用 sayHelloTo 方法
this.helloBackendService.sayHelloTo('World').then(r => console.log(r);

electron-browser 调用 electron-main 也是类似的,具体的可以参考:https://www.yuque.com/zhaomenghuan/theia/qalple#FWYNK

Theia Plug-ins

Theia Plug-ins 入门

Theia 和 VS Code 拓展能力区别较大,Theia 通过 Extension 机制提供对应用内部的拓展能力,通过 Plug-ins 机制提供外部的拓展能力;Theia 的 Plug-ins 机制和 VS Code 的拓展机制类似,同时 Theia Plug-ins 对 VS Code 拓展兼容支持。

Plug-ins 相比 Extensions 的优势:

  • 代码隔离:Plug-ins 作为插件在单独进程中运行的代码,它不能阻止 Theia 核心进程;
  • 可以在运行时加载:无需重新编译Theia的完整IDE;
  • 减少编译时间:Theia 应用引入的插件无需进行编译;
  • 独立发布:插件可以打包成一个文件,然后直接加载,无需额外获取 npmjs 等的依赖项;
  • API 方式开发:无需学习 inversify 或其他框架;API 向后兼容,可以轻松进行 Theia 版本升级。

Theia 应用程序由一个内核组成,该内核提供了一组用于特定功能的小部件,命令,处理程序等。Theia 定义了一个运行时 API,允许插件自定义 IDE 并将其行为添加到应用程序的各个方面。在 Theia 中,插件可以通过名为theia 的对象访问 API,该对象在所有插件中都可用。Theia 可用的 API 使用文档:@theia/plugin,Theia API 兼容 VS Code API,API 覆盖率文档:Compare Theia vs VS Code API。

Theia 在技术架构上分成前端和后端两大部分,对于插件体系也是类似的,分为 Frontend plug-in 和 Backend plug-in。前端插件是工作在 Browser 的 UI线程,因此无法直接打开或写入文件;后端插件的代码在服务器端以专用进程运行,后端插件调用 API 后,将在用户的浏览器/ UI上发送一些操作以注册新命令等。后端插件和 VS Code 的 Extensions 类似。

Theia Plug-ins Hello World

根据 《Authoring Theia Plug-ins》的文档创建插件工程。

npm install -g yo @theia/generator-plugin
mkdir theia-hello-world-plugin
cd theia-hello-world-plugin
yo @theia/plugin

未完待续。。。

参考

  • KAITIAN IDE 是如何构建扩展能力极强的插件体系的?
  • 开发一个爆款 VSCode 插件

Eclipse Theia 揭秘之拓展机制篇相关推荐

  1. Eclipse Theia 揭秘之启动流程篇

    前言 在<Eclipse Theia 框架技术架构揭秘>一文中简单介绍了 Theia 框架整体的技术架构,接下来将通过系列文章从源码角度入手看一下 Theia 核心设计思路,本文从启动流程 ...

  2. Eclipse Theia 揭秘之技术架构篇

    Cloud IDE 随着前端开发的发展更迭,前端日常开发工作变得愈发复杂愈发深入,同时前端工程中从项目初始化.编译.构建到发布.运维也变得细化而成熟.本地开发环境存在开发机性能要求高.开发环境配置复杂 ...

  3. Eclipse Theia技术揭秘——脚手架源码分析

    在之前的文章中,我们介绍了Theia的构建,其中用到了很多theia的命令,这些命令来自于@theia/cli这个库,本篇文章我们就对@theia/cli以及相关联的库进行分析.本篇文章是继构建桌面I ...

  4. Eclipse Theia vs. VS Code:“ Theia是最多样化,最活跃的项目之一”

    JAXenter: Eclipse Theia 1.0版刚刚发布. 在JAXenter上,我们已经关注Eclipse Theia一段时间了,因此我们对该主题的处理会有所不同. 在正式公告中,值得注意的 ...

  5. 红茶一杯话Binder(传输机制篇_上)

    2019独角兽企业重金招聘Python工程师标准>>> 红茶一杯话Binder (传输机制篇_上) 侯 亮 1 Binder是如何做到精确打击的? 我们先问一个问题,binder机制 ...

  6. IDEA 上位?不!Eclipse Theia 1.0 发布!

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:硬刚一周,3W字总结,一年的经验告诉你如何准备校招! 个人原创100W+访问量博客:点击前往,查看更多 来源:开 ...

  7. 红茶一杯话Binder(传输机制篇_下)

    1 事务的传递和处理 从IPCThreadState的角度看,它的transact()函数是通过向binder驱动发出BC_TRANSACTION语义,来表达其传输意图的,而后如有必要,它会等待从bi ...

  8. Eclipse BIRT使用之BIRT Designer篇(转)

    Eclipse BIRT使用之BIRT Designer篇 在开源的报表工具中,Eclipse的BIRT(Business Intelligence and Reporting Tools ,简称BI ...

  9. eclipse theia_如何在Ubuntu 18.04上设置Eclipse Theia Cloud IDE平台[快速入门]

    eclipse theia 介绍 (Introduction) Eclipse Theia is an extensible cloud IDE running on a remote server ...

最新文章

  1. PL/SQL程序设计 第七章 包的创建和应用
  2. Android App监听软键盘按键的三种方式(转)
  3. 找到那些新时代的“操作系统”
  4. 每日一皮:在调试时,将断点设置在错误的位置...
  5. ca证书 linux 导入_Linux CA证书服务器搭建
  6. MySQL数据库序列的作用_MySQL数据库:序列使用
  7. 站在巨人的肩膀,2020我在使用和涉及到的开源项目
  8. PNAS新研究:剑桥学者发现,有些 AI 模型无法被计算
  9. 社区团购如今进行得如火如荼
  10. VB6.0动态加载ActiveX控件漫谈[转]
  11. 【Oracle】数据迁移工具(2):Data Dump
  12. 【注意】关于fgets函数
  13. 伪元素::selection -- CSS ::selection 伪元素,定义用户鼠标已选择内容的样式
  14. 嵌入式Linux开发板移植SSH
  15. arcmap中加载底图
  16. 开心网(kaixin001)上的X世界小游戏
  17. nginx 引号 x22
  18. mysql 时区时间_mysql的时间不对(时区问题)
  19. 工作中如果一直被领导忽略,你会怎么办?
  20. 关于微信小程序danmu-List所遇到的坑

热门文章

  1. 30.JavaScript数组基础、遍历、底层实现、push、pop、at、length
  2. C#邮件接收与发送程序
  3. Norton 360 正式版完整揭密
  4. ping : www. baidu .com: Name or service not knowm
  5. 无线无源温度传感器项目-软件
  6. 学点实用工作小技巧【Python】汉字转拼音、繁体字和简体字互转、提取字符串中的中文(英文)、判断是否纯中文(英文)
  7. 上海汉得公司2018年秋招前端笔试题
  8. Faster RCNN笔记
  9. rx590 黑苹果 无货_黑苹果10.14免驱显卡表
  10. 特斯拉家用充电桩电流是多少_特斯拉如何给家庭装上充电桩