vscode 高亮 原理

2020-11-04  本文已影响0人  月半女那

demo思路

  1. 从源文件中把关键字识别出来
  2. 如何渲染识别出来的高亮部分
  1. 去识别关键字:直接基于正则扫描 (目前众包的实现) / 基于AST直接渲染
  2. 基于 html element 的方案
    基于 svg 的方案
    基于 类似 ace editor的编辑器组件,开箱即用,只需要传递需要渲染的文本和高亮规则

高亮

语法高亮由两部分工作组成:

语法高亮其实是有两种实现方案,

  1. 一种是基于正则,原文直接匹配,匹配的结果直接替换成富文本(例如带样式的html标签),最终会得到一个关键字被高亮的富文本。
  2. 第二种,源文件调用paser 处理成AST,然后用AST去渲染,生成富文本。

第一种方案更适合于语法简单,不包含上下文关键字的情况,例如在c#中这类情况 add , group这类情况在非linq的上下文,是不应该被渲染成关键字。
第二种方案可以完美解决这种情况,AST中包含了上下文信息,有助于判断是否应该是关键字的情况

AST

AST(Abstract Syntax Tree 抽象语法树) , 它是源代码语法结构的一种抽象标表示,它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。

.用处
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

使用语言服务器协议的语言服务器。它的实现方式如下:

这个过程可能看起来有些复杂,但是这么做主要有两个好处:

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在保持核心轻量级的基础上,增加项目支持,智能感知,编译调试。

├── 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

核心层

核心环境

整个项目完全使用typescript实现,electron中运行主进程和渲染进程,使用的api有所不同,所以在core中每个目录组织也是按照使用的api来安排, 运行的环境分为几类:

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);
            }
        }
    }
}
上一篇下一篇

猜你喜欢

热点阅读