vscode 高亮 原理
demo思路
- 需求:语法高亮本质是把源文件中的关键字等具有语法意义的特殊字符序列渲染出来。
- 思路:
- 从源文件中把关键字识别出来
- 如何渲染识别出来的高亮部分
- 解决方案
- 去识别关键字:直接基于正则扫描 (目前众包的实现) / 基于AST直接渲染
- 基于 html element 的方案
基于 svg 的方案
基于 类似 ace editor的编辑器组件,开箱即用,只需要传递需要渲染的文本和高亮规则
高亮
语法高亮由两部分工作组成:
- 根据语法将文本解析成符号和作用域
- 然后根据这份作用域映射应用对应的颜色和样式
语法高亮其实是有两种实现方案,
- 一种是基于正则,原文直接匹配,匹配的结果直接替换成富文本(例如带样式的html标签),最终会得到一个关键字被高亮的富文本。
- 第二种,源文件调用paser 处理成AST,然后用AST去渲染,生成富文本。
第一种方案更适合于语法简单,不包含上下文关键字的情况,例如在c#中这类情况 add , group这类情况在非linq的上下文,是不应该被渲染成关键字。
第二种方案可以完美解决这种情况,AST中包含了上下文信息,有助于判断是否应该是关键字的情况
AST
AST(Abstract Syntax Tree 抽象语法树) , 它是源代码语法结构的一种抽象标表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。
.用处
- 最初是为了实现某种编程语言的编辑器所需要的语法中介
- 编辑器的错误提示,代码高亮,自动补全,
- eslint等对代码风格和格式的检查
- webpack通过babel转译js语法
AST 如何生成
编译器执行的第一步是读取文件中的字符流,然后通过词法分析生成 token,之后再通过语法分析( Parser )生成 AST,最后生成机器码执行。
- 分词:将整个代码字符串分割成最小语法单元数组
- 语法分析:在分词基础上建立分析语法单元之间的关系
词法分析
词法分析:也称之为扫描(scanner),简单来说就是调用 next() 方法,一个一个字母的来读取字符,然后与定义好的关键字符做比较,生成对应的Token。Token 是一个不可分割的最小单元:
词法分析器里,每个关键字是一个 Token ,每个标识符是一个 Token,每个操作符是一个 Token,每个标点符号也都是一个 Token。除此之外,还会过滤掉源程序中的注释和空白字符(换行符、空格、制表符等。)
最终,整个代码将被分割进一个tokens列表(或者说一维数组)。
语法分析
语法分析会将词法分析出来的 Token 转化成有语法含义的抽象语法树结构。同时,验证语法,语法如果有错的话,抛出语法错误。
demo 演示
LSP( LSP (Language Server Protocol))
LSP 是 微软为解决 IDE 语言服务和调试适配器 M x N 问题, 传统的每个 IDE 都要自行开发一套某个语言的语言服务程序和调试适配器, 而这些语言服务程序都使用不同的接口, 完全无法复用, 造成各大 IDE 开发成本过高的问题.
通俗的讲就是语言服务单独运行在一个进程里,通过 JSON RPC 作为协议与客户端通信,为其提供如跳转定义、自动补全等通用语言功能,例如 ts 的类型检查、类型跳转、自动补全等都需要有对应的 ts 语言服务端实现并与 Client 端通信。
language-server-protocol.png使用语言服务器协议的语言服务器。它的实现方式如下:
- 一个为JS同时提供语言客户端和语言服务器的插件
- 语言客户端就像普通插件一样,运行于Node.js插件主机环境中。这个插件激活后,会启动另一个进程——语言服务器,然后两者通过语言服务器协议进行通信。
- 你悬停到JS代码上
- VS Code通知语言客户端
- 语言客户端向语言服务器发起请求,索要悬停的返回结果,最后再送回给VS Code
- VS Code将结果展示在悬浮框中
这个过程可能看起来有些复杂,但是这么做主要有两个好处:
- 语言服务器可以用任何语言实现
- 语言服务器可以被多个编辑器重用,提供更加智能的编辑体验
language server
Language Server翻译为“语言服务器”,并不是说它真的是一个服务器,而是它把语言相关的特性和功能从IDE中解耦出来,作为一个独立的程序单独运行,提供了例如引用查询(Find All References)等功能的具体实现,Client是编辑器或IDE,例如Atom、VScode等。
更加确切的解释是,Language Server是某语言的Language Server Protocol具体实现。
vscode
Visual Studio Code(简称VSCode) 是开源免费的IDE编辑器,原本是微软内部使用的云编辑器(Monaco)。
git仓库地址: https://github.com/microsoft/vscode
通过Eletron集成了桌面应用,可以跨平台使用,开发语言主要采用微软自家的TypeScript。 整个项目结构比较清晰,方便阅读代码理解。成为了最流行跨平台的桌面IDE应用
微软希望VSCode在保持核心轻量级的基础上,增加项目支持,智能感知,编译调试。
- TypeScript是一种由微软开发的自由和开源的编程语言。它是JavaScript的一个超集,而且本质上向这个语言添加了可选的静态类型和基于类的面向对象编程
├── build # gulp编译构建脚本
├── extensions # 内置插件
├── product.json # App meta信息
├── resources # 平台相关静态资源
├── scripts # 工具脚本,开发/测试
├── src # 源码目录
└── typings # 函数语法补全定义
└── vs
├── base # 通用工具/协议和UI库
│ ├── browser # 基础UI组件,DOM操作
│ ├── common # diff描述,markdown解析器,worker协议,各种工具函数
│ ├── node # Node工具函数
│ ├── parts # IPC协议(Electron、Node),quickopen、tree组件
│ ├── test # base单测用例
│ └── worker # Worker factory和main Worker(运行IDE Core:Monaco)
├── code # VSCode主运行窗口
├── editor # IDE代码编辑器
| ├── browser # 代码编辑器核心
| ├── common # 代码编辑器核心
| ├── contrib # vscode 与独立 IDE共享的代码
| └── standalone # 独立 IDE 独有的代码
├── platform # 支持注入服务和平台相关基础服务(文件、剪切板、窗体、状态栏)
├── workbench # 工作区UI布局,功能主界面
│ ├── api #
│ ├── browser #
│ ├── common #
│ ├── contrib #
│ ├── electron-browser #
│ ├── services #
│ └── test #
├── css.build.js # 用于插件构建的CSS loader
├── css.js # CSS loader
├── editor # 对接IDE Core(读取编辑/交互状态),提供命令、上下文菜单、hover、snippet等支持
├── loader.js # AMD loader(用于异步加载AMD模块)
├── nls.build.js # 用于插件构建的NLS loader
└── nls.js # NLS(National Language Support)多语言loader
核心层
- base: 提供通用服务和构建用户界面
- platform: 注入服务和基础服务代码
- editor: 微软Monaco编辑器,也可独立运行使用
- wrokbench: 配合Monaco并且给viewlets提供框架:如:浏览器状态栏,菜单栏利用electron实现桌面程序
核心环境
整个项目完全使用typescript实现,electron中运行主进程和渲染进程,使用的api有所不同,所以在core中每个目录组织也是按照使用的api来安排, 运行的环境分为几类:
- common: 只使用javascritp api的代码,能在任何环境下运行
- browser: 浏览器api, 如操作dom; 可以调用common
- node: 需要使用node的api,比如文件io操作
- electron-brower: 渲染进程api, 可以调用common, brower, node, 依赖electron renderer-process API
- electron-main: 主进程api, 可以调用: common, node 依赖于electron main-process AP
vscode事件分发
src/vs/base/common/event.ts
程序中常见使用once方法进行事件绑定, 给定一个事件,返回一个只触发一次的事件,放在匿名函数返回
export function once<T>(event: Event<T>): Event<T> {
return (listener, thisArgs = null, disposables?) => {
// 设置次变量,防止事件重复触发造成事件污染
let didFire = false;
let result: IDisposable;
result = event(e => {
if (didFire) {
return;
} else if (result) {
result.dispose();
} else {
didFire = true;
}
return listener.call(thisArgs, e);
}, null, disposables);
if (didFire) {
result.dispose();
}
return result;
};
}
循环派发了所有注册的事件, 事件会存储到一个事件队列,通过fire方法触发事件
private _deliveryQueue?: LinkedList<[Listener, T]>;//事件存储队列
fire(event: T): void {
if (this._listeners) {
// 将所有事件传入 delivery queue
// 内部/嵌套方式通过emit发出.
// this调用事件驱动
if (!this._deliveryQueue) {
this._deliveryQueue = new LinkedList();
}
for (let iter = this._listeners.iterator(), e = iter.next(); !e.done; e = iter.next()) {
this._deliveryQueue.push([e.value, event]);
}
while (this._deliveryQueue.size > 0) {
const [listener, event] = this._deliveryQueue.shift()!;
try {
if (typeof listener === 'function') {
listener.call(undefined, event);
} else {
listener[0].call(listener[1], event);
}
} catch (e) {
onUnexpectedError(e);
}
}
}
}