Vscode源码与插件详解


Keyboard Reference Sheets

Download the keyboard shortcut reference sheet for your platform (macOS, Windows, Linux).

🤩Vscode源码分析

简介

Visual Studio Code(简称VSCode) 是开源免费的IDE编辑器,原本是微软内部使用的云编辑器(Monaco)。

git仓库地址: https://github.com/microsoft/vscode

通过Eletron集成了桌面应用,可以跨平台使用,开发语言主要采用微软自家的TypeScript。 整个项目结构比较清晰,方便阅读代码理解。成为了最流行跨平台的桌面IDE应用

微软希望VSCode在保持核心轻量级的基础上,增加项目支持,智能感知,编译调试。

编译安装

下载最新版本,目前我用的是1.37.1版本 官方的wiki中有编译安装的说明 How to Contribute

Linux, Window, MacOS三个系统编译时有些差别,参考官方文档, 在编译安装依赖时如果遇到connect timeout, 需要进行科学上网。

需要注意的一点 运行环境依赖版本 Nodejs x64 version >= 10.16.0, < 11.0.0, python 2.7(3.0不能正常执行)

技术架构

Electron

Electron 是一个使用 JavaScript, HTML 和 CSS 等 Web 技术创建原生程序的框架,它负责比较难搞的部分,你只需把精力放在你的应用的核心上即可 (Electron = Node.js + Chromium + Native API)

Monaco Editor

Monaco Editor是微软开源项目, 为VS Code提供支持的代码编辑器,运行在浏览器环境中。编辑器提供代码提示,智能建议等功能。供开发人员远程更方便的编写代码,可独立运行。

TypeScript

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

主启动流程

Electron通过package.json中的main字段来定义应用入口。

main.js是vscode的入口。

  • src/main.js
    • vs/code/electron-main/main.ts
    • vs/code/electron-main/app.ts
    • vs/code/electron-main/windows.ts
    • vs/workbench/electron-browser/desktop.main.ts
    • vs/workbench/browser/workbench.ts
app.once('ready', function () {
    //启动追踪,后面会讲到,跟性能检测优化相关。
    if (args['trace']) {
        // @ts-ignore
        const contentTracing = require('electron').contentTracing;
        const traceOptions = {
            categoryFilter: args['trace-category-filter'] || '*',
            traceOptions: args['trace-options'] || 'record-until-full,enable-sampling'
        };
        contentTracing.startRecording(traceOptions, () => onReady());
    } else {
        onReady();
    }
});
function onReady() {
    perf.mark('main:appReady');
    Promise.all([nodeCachedDataDir.ensureExists(), userDefinedLocale]).then(([cachedDataDir, locale]) => {
        //1. 这里尝试获取本地配置信息,如果有的话会传递到startup
        if (locale && !nlsConfiguration) {
            nlsConfiguration = lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, locale);
        }
        if (!nlsConfiguration) {
            nlsConfiguration = Promise.resolve(undefined);
        }
        nlsConfiguration.then(nlsConfig => {
            //4. 首先会检查用户语言环境配置,如果没有设置默认使用英语 
            const startup = nlsConfig => {
                nlsConfig._languagePackSupport = true;
                process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
                process.env['VSCODE_NODE_CACHED_DATA_DIR'] = cachedDataDir || '';
                perf.mark('willLoadMainBundle');
                //使用微软的loader组件加载electron-main/main文件
                require('./bootstrap-amd').load('vs/code/electron-main/main', () => {
                    perf.mark('didLoadMainBundle');
                });
            };
            // 2. 接收到有效的配置传入是其生效,调用startup
            if (nlsConfig) {
                startup(nlsConfig);
            }
            // 3. 这里尝试使用本地的应用程序
            // 应用程序设置区域在ready事件后才有效
            else {
                let appLocale = app.getLocale();
                if (!appLocale) {
                    startup({ locale: 'en', availableLanguages: {} });
                } else {
                    // 配置兼容大小写敏感,所以统一转换成小写
                    appLocale = appLocale.toLowerCase();
                    // 这里就会调用config服务,把本地配置加载进来再调用startup
                    lp.getNLSConfiguration(product.commit, userDataPath, metaDataFile, appLocale).then(nlsConfig => {
                        if (!nlsConfig) {
                            nlsConfig = { locale: appLocale, availableLanguages: {} };
                        }
                        startup(nlsConfig);
                    });
                }
            }
        });
    }, console.error);
}

vs/code/electron-main/main.ts

electron-main/main 是程序真正启动的入口,进入main process初始化流程.

这里主要做了两件事情:

  1. 初始化Service
  2. 启动主实例

直接看startup方法的实现,基础服务初始化完成后会加载 CodeApplication, mainIpcServer, instanceEnvironment,调用 startup 方法启动APP

private async startup(args: ParsedArgs): Promise<void> {
        //spdlog 日志服务
        const bufferLogService = new BufferLogService();
        // 1. 调用 createServices
        const [instantiationService, instanceEnvironment] = this.createServices(args, bufferLogService);
        try {
            // 1.1 初始化Service服务
            await instantiationService.invokeFunction(async accessor => {
                // 基础服务,包括一些用户数据,缓存目录
                const environmentService = accessor.get(IEnvironmentService);
                // 配置服务
                const configurationService = accessor.get(IConfigurationService);
                // 持久化数据
                const stateService = accessor.get(IStateService);
                try {
                    await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService);
                } catch (error) {
                    // 抛出错误对话框
                    this.handleStartupDataDirError(environmentService, error);
                    throw error;
                }
            });
            // 1.2 启动实例
            await instantiationService.invokeFunction(async accessor => {
                const environmentService = accessor.get(IEnvironmentService);
                const logService = accessor.get(ILogService);
                const lifecycleService = accessor.get(ILifecycleService);
                const configurationService = accessor.get(IConfigurationService);
                const mainIpcServer = await this.doStartup(logService, environmentService, lifecycleService, instantiationService, true);
                bufferLogService.logger = new SpdLogService('main', environmentService.logsPath, bufferLogService.getLevel());
                once(lifecycleService.onWillShutdown)(() => (configurationService as ConfigurationService).dispose());
                return instantiationService.createInstance(CodeApplication, mainIpcServer, instanceEnvironment).startup();
            });
        } catch (error) {
            instantiationService.invokeFunction(this.quit, error);
        }
    }

vs/code/electron-main/app.ts

这里首先触发CodeApplication.startup()方法, 在第一个窗口打开3秒后成为共享进程,

async startup(): Promise<void> {
    ...
    // 1. 第一个窗口创建共享进程
    const sharedProcess = this.instantiationService.createInstance(SharedProcess, machineId, this.userEnv);
    const sharedProcessClient = sharedProcess.whenReady().then(() => connect(this.environmentService.sharedIPCHandle, 'main'));
    this.lifecycleService.when(LifecycleMainPhase.AfterWindowOpen).then(() => {
        this._register(new RunOnceScheduler(async () => {
            const userEnv = await getShellEnvironment(this.logService, this.environmentService);
            sharedProcess.spawn(userEnv);
        }, 3000)).schedule();
    });
    // 2. 创建app实例
    const appInstantiationService = await this.createServices(machineId, trueMachineId, sharedProcess, sharedProcessClient);
    // 3. 打开一个窗口 调用 
    const windows = appInstantiationService.invokeFunction(accessor => this.openFirstWindow(accessor, electronIpcServer, sharedProcessClient));
    // 4. 窗口打开后执行生命周期和授权操作
    this.afterWindowOpen();
    ...
    //vscode结束了性能问题的追踪
    if (this.environmentService.args.trace) {
        this.stopTracingEventually(windows);
    }
}

openFirstWindow 主要实现 CodeApplication.openFirstWindow 首次开启窗口时,创建 Electron 的 IPC,使主进程和渲染进程间通信。 window会被注册到sharedProcessClient,主进程和共享进程通信 根据 environmentService 提供的参数(path,uri)调用windowsMainService.open 方法打开窗口

private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<Client<string>>): ICodeWindow[] {
        ...
        // 1. 注入Electron IPC Service, windows窗口管理,菜单栏等服务
        // 2. 根据environmentService进行参数配置
        const macOpenFiles: string[] = (<any>global).macOpenFiles;
        const context = !!process.env['VSCODE_CLI'] ? OpenContext.CLI : OpenContext.DESKTOP;
        const hasCliArgs = hasArgs(args._);
        const hasFolderURIs = hasArgs(args['folder-uri']);
        const hasFileURIs = hasArgs(args['file-uri']);
        const noRecentEntry = args['skip-add-to-recently-opened'] === true;
        const waitMarkerFileURI = args.wait && args.waitMarkerFilePath ? URI.file(args.waitMarkerFilePath) : undefined;
        ...
        // 打开主窗口,默认从执行命令行中读取参数 
        return windowsMainService.open({
            context,
            cli: args,
            forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
            diffMode: args.diff,
            noRecentEntry,
            waitMarkerFileURI,
            gotoLineMode: args.goto,
            initialStartup: true
        });
    }

vs/code/electron-main/windows.ts

接下来到了electron的windows窗口,open方法在doOpen中执行窗口配置初始化,最终调用openInBrowserWindow -> 执行doOpenInBrowserWindow是其打开window,主要步骤如下:

private openInBrowserWindow(options: IOpenBrowserWindowOptions): ICodeWindow {
    ...
    // New window
    if (!window) {
        //1.判断是否全屏创建窗口
         ...
        // 2. 创建实例窗口
        window = this.instantiationService.createInstance(CodeWindow, {
            state,
            extensionDevelopmentPath: configuration.extensionDevelopmentPath,
            isExtensionTestHost: !!configuration.extensionTestsPath
        });
        // 3.添加到当前窗口控制器
        WindowsManager.WINDOWS.push(window);
        // 4.窗口监听器
        window.win.webContents.removeAllListeners('devtools-reload-page'); // remove built in listener so we can handle this on our own
        window.win.webContents.on('devtools-reload-page', () => this.reload(window!));
        window.win.webContents.on('crashed', () => this.onWindowError(window!, WindowError.CRASHED));
        window.win.on('unresponsive', () => this.onWindowError(window!, WindowError.UNRESPONSIVE));
        window.win.on('closed', () => this.onWindowClosed(window!));
        // 5.注册窗口生命周期
        (this.lifecycleService as LifecycleService).registerWindow(window);
    }
    ...
    return window;
}

doOpenInBrowserWindow会调用window.load方法 在window.ts中实现

load(config: IWindowConfiguration, isReload?: boolean, disableExtensions?: boolean): void {
    ...
    // Load URL
    perf.mark('main:loadWindow');
    this._win.loadURL(this.getUrl(configuration));
    ...
}
private getUrl(windowConfiguration: IWindowConfiguration): string {
    ...
    //加载欢迎屏幕的html
    let configUrl = this.doGetUrl(config);
    ...
    return configUrl;
}
//默认加载 vs/code/electron-browser/workbench/workbench.html
private doGetUrl(config: object): string {
    return `${require.toUrl('vs/code/electron-browser/workbench/workbench.html')}?config=${encodeURIComponent(JSON.stringify(config))}`;
}

main process的使命完成, 主界面进行构建布局。

在workbench.html中加载了workbench.js, 这里调用return require(‘vs/workbench/electron-browser/desktop.main’).main(configuration);实现对主界面的展示

vs/workbench/electron-browser/desktop.main.ts

创建工作区,调用workbench.startup()方法,构建主界面展示布局

...
async open(): Promise<void> {
    const services = await this.initServices();
    await domContentLoaded();
    mark('willStartWorkbench');
    // 1.创建工作区
    const workbench = new Workbench(document.body, services.serviceCollection, services.logService);
    // 2.监听窗口变化
    this._register(addDisposableListener(window, EventType.RESIZE, e => this.onWindowResize(e, true, workbench)));
    // 3.工作台生命周期
    this._register(workbench.onShutdown(() => this.dispose()));
    this._register(workbench.onWillShutdown(event => event.join(services.storageService.close())));
    // 3.启动工作区
    const instantiationService = workbench.startup();
    ...
}
...

vs/workbench/browser/workbench.ts

工作区继承自layout类,主要作用是构建工作区,创建界面布局。

export class Workbench extends Layout {
    ...
    startup(): IInstantiationService {
        try {
            ...
            // Services
            const instantiationService = this.initServices(this.serviceCollection);
            instantiationService.invokeFunction(async accessor => {
                const lifecycleService = accessor.get(ILifecycleService);
                const storageService = accessor.get(IStorageService);
                const configurationService = accessor.get(IConfigurationService);
                // Layout
                this.initLayout(accessor);
                // Registries
                this.startRegistries(accessor);
                // Context Keys
                this._register(instantiationService.createInstance(WorkbenchContextKeysHandler));
                // 注册监听事件
                this.registerListeners(lifecycleService, storageService, configurationService);
                // 渲染工作区
                this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService);
                // 创建工作区布局
                this.createWorkbenchLayout(instantiationService);
                // 布局构建
                this.layout();
                // Restore
                try {
                    await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService);
                } catch (error) {
                    onUnexpectedError(error);
                }
            });
            return instantiationService;
        } catch (error) {
            onUnexpectedError(error);
            throw error; // rethrow because this is a critical issue we cannot handle properly here
        }
    }
    ...
}

实例化服务

SyncDescriptor负责注册这些服务,当用到该服务时进程实例化使用

src/vs/platform/instantiation/common/descriptors.ts

export class SyncDescriptor<T> {
    readonly ctor: any;
    readonly staticArguments: any[];
    readonly supportsDelayedInstantiation: boolean;
    constructor(ctor: new (...args: any[]) => T, staticArguments: any[] = [], supportsDelayedInstantiation: boolean = false) {
        this.ctor = ctor;
        this.staticArguments = staticArguments;
        this.supportsDelayedInstantiation = supportsDelayedInstantiation;
    }
}

main.ts中startup方法调用invokeFunction.get实例化服务

await instantiationService.invokeFunction(async accessor => {
    const environmentService = accessor.get(IEnvironmentService);
    const configurationService = accessor.get(IConfigurationService);
    const stateService = accessor.get(IStateService);
    try {
        await this.initServices(environmentService, configurationService as ConfigurationService, stateService as StateService);
    } catch (error) {
        // Show a dialog for errors that can be resolved by the user
        this.handleStartupDataDirError(environmentService, error);
        throw error;
    }
});

get方法调用_getOrCreateServiceInstance,这里第一次创建会存入缓存中 下次实例化对象时会优先从缓存中获取对象。

src/vs/platform/instantiation/common/instantiationService.ts

invokeFunction<R, TS extends any[] = []>(fn: (accessor: ServicesAccessor, ...args: TS) => R, ...args: TS): R {
    let _trace = Trace.traceInvocation(fn);
    let _done = false;
    try {
        const accessor: ServicesAccessor = {
            get: <T>(id: ServiceIdentifier<T>, isOptional?: typeof optional) => {
                if (_done) {
                    throw illegalState('service accessor is only valid during the invocation of its target method');
                }
                const result = this._getOrCreateServiceInstance(id, _trace);
                if (!result && isOptional !== optional) {
                    throw new Error(`[invokeFunction] unknown service '${id}'`);
                }
                return result;
            }
        };
        return fn.apply(undefined, [accessor, ...args]);
    } finally {
        _done = true;
        _trace.stop();
    }
}
private _getOrCreateServiceInstance<T>(id: ServiceIdentifier<T>, _trace: Trace): T {
    let thing = this._getServiceInstanceOrDescriptor(id);
    if (thing instanceof SyncDescriptor) {
        return this._createAndCacheServiceInstance(id, thing, _trace.branch(id, true));
    } else {
        _trace.branch(id, false);
        return thing;
    }
}

事件分发

event

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);
            }
        }
    }
}

进程通信

主进程

src/vs/code/electron-main/main.ts

main.ts在启动应用后就创建了一个主进程 main process,它可以通过electron中的一些模块直接与原生GUI交互。

server = await serve(environmentService.mainIPCHandle);
once(lifecycleService.onWillShutdown)(() => server.dispose());

渲染进程

仅启动主进程并不能给你的应用创建应用窗口。窗口是通过main文件里的主进程调用叫BrowserWindow的模块创建的。

主进程与渲染进程之间的通信

在electron中,主进程与渲染进程有很多通信的方法。比如ipcRenderer和ipcMain,还可以在渲染进程使用remote模块。

ipcMain & ipcRenderer

  • 主进程:ipcMain
  • 渲染进程:ipcRenderer

ipcMain模块和ipcRenderer是类EventEmitter的实例。

在主进程中使用ipcMain接收渲染线程发送过来的异步或同步消息,发送过来的消息将触发事件。

在渲染进程中使用ipcRenderer向主进程发送同步或异步消息,也可以接收到主进程的消息。

  • 发送消息,事件名为 channel .
  • 回应同步消息, 你可以设置 event.returnValue .
  • 回应异步消息, 你可以使用 event.sender.send(…)

创建IPC服务 src/vs/base/parts/ipc/node/ipc.net.ts

这里返回一个promise对象,成功则createServer

export function serve(hook: any): Promise<Server> {
    return new Promise<Server>((c, e) => {
        const server = createServer();
        server.on('error', e);
        server.listen(hook, () => {
            server.removeListener('error', e);
            c(new Server(server));
        });
    });
}

创建信道

src/vs/code/electron-main/app.ts

  • mainIpcServer
    • launchChannel
  • electronIpcServer
    • updateChannel
    • issueChannel
    • workspacesChannel
    • windowsChannel
    • menubarChannel
    • urlChannel
    • storageChannel
    • logLevelChannel
private openFirstWindow(accessor: ServicesAccessor, electronIpcServer: ElectronIPCServer, sharedProcessClient: Promise<Client<string>>): ICodeWindow[] {
        // Register more Main IPC services
        const launchService = accessor.get(ILaunchService);
        const launchChannel = new LaunchChannel(launchService);
        this.mainIpcServer.registerChannel('launch', launchChannel);
        // Register more Electron IPC services
        const updateService = accessor.get(IUpdateService);
        const updateChannel = new UpdateChannel(updateService);
        electronIpcServer.registerChannel('update', updateChannel);
        const issueService = accessor.get(IIssueService);
        const issueChannel = new IssueChannel(issueService);
        electronIpcServer.registerChannel('issue', issueChannel);
        const workspacesService = accessor.get(IWorkspacesMainService);
        const workspacesChannel = new WorkspacesChannel(workspacesService);
        electronIpcServer.registerChannel('workspaces', workspacesChannel);
        const windowsService = accessor.get(IWindowsService);
        const windowsChannel = new WindowsChannel(windowsService);
        electronIpcServer.registerChannel('windows', windowsChannel);
        sharedProcessClient.then(client => client.registerChannel('windows', windowsChannel));
        const menubarService = accessor.get(IMenubarService);
        const menubarChannel = new MenubarChannel(menubarService);
        electronIpcServer.registerChannel('menubar', menubarChannel);
        const urlService = accessor.get(IURLService);
        const urlChannel = new URLServiceChannel(urlService);
        electronIpcServer.registerChannel('url', urlChannel);
        const storageMainService = accessor.get(IStorageMainService);
        const storageChannel = this._register(new GlobalStorageDatabaseChannel(this.logService, storageMainService));
        electronIpcServer.registerChannel('storage', storageChannel);
        // Log level management
        const logLevelChannel = new LogLevelSetterChannel(accessor.get(ILogService));
        electronIpcServer.registerChannel('loglevel', logLevelChannel);
        sharedProcessClient.then(client => client.registerChannel('loglevel', logLevelChannel));
        ...
        // default: read paths from cli
        return windowsMainService.open({
            context,
            cli: args,
            forceNewWindow: args['new-window'] || (!hasCliArgs && args['unity-launch']),
            diffMode: args.diff,
            noRecentEntry,
            waitMarkerFileURI,
            gotoLineMode: args.goto,
            initialStartup: true
        });
    }

每一个信道,内部实现两个方法 listen和call

例如:src/vs/platform/localizations/node/localizationsIpc.ts

构造函数绑定事件

export class LocalizationsChannel implements IServerChannel {
    onDidLanguagesChange: Event<void>;
    constructor(private service: ILocalizationsService) {
        this.onDidLanguagesChange = Event.buffer(service.onDidLanguagesChange, true);
    }
    listen(_: unknown, event: string): Event<any> {
        switch (event) {
            case 'onDidLanguagesChange': return this.onDidLanguagesChange;
        }
        throw new Error(`Event not found: ${event}`);
    }
    call(_: unknown, command: string, arg?: any): Promise<any> {
        switch (command) {
            case 'getLanguageIds': return this.service.getLanguageIds(arg);
        }
        throw new Error(`Call not found: ${command}`);
    }
}

主要窗口

workbench.ts中startup里面Workbench负责创建主界面 src/vs/workbench/browser/workbench.ts

startup(): IInstantiationService {
    try {
        ...
        instantiationService.invokeFunction(async accessor => {
            // 渲染主工作界面
            this.renderWorkbench(instantiationService, accessor.get(INotificationService) as NotificationService, storageService, configurationService);
            // 界面布局
            this.createWorkbenchLayout(instantiationService);
            // Layout
            this.layout();
            // Restore
            try {
                await this.restoreWorkbench(accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IViewletService), accessor.get(IPanelService), accessor.get(ILogService), lifecycleService);
            } catch (error) {
                onUnexpectedError(error);
            }
        });
        return instantiationService;
    } catch (error) {
        onUnexpectedError(error);
        throw error; // rethrow because this is a critical issue we cannot handle properly here
    }
}

渲染主工作台,渲染完之后加入到container中,container加入到parent, parent就是body了。

this.parent.appendChild(this.container);

private renderWorkbench(instantiationService: IInstantiationService, notificationService: NotificationService, storageService: IStorageService, configurationService: IConfigurationService): void {
        ...
        //TITLEBAR_PART 顶部操作栏
        //ACTIVITYBAR_PART 最左侧菜单选项卡
        //SIDEBAR_PART 左侧边栏,显示文件,结果展示等
        //EDITOR_PART 右侧窗口,代码编写,欢迎界面等
        //STATUSBAR_PART 底部状态栏
        [
            { id: Parts.TITLEBAR_PART, role: 'contentinfo', classes: ['titlebar'] },
            { id: Parts.ACTIVITYBAR_PART, role: 'navigation', classes: ['activitybar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] },
            { id: Parts.SIDEBAR_PART, role: 'complementary', classes: ['sidebar', this.state.sideBar.position === Position.LEFT ? 'left' : 'right'] },
            { id: Parts.EDITOR_PART, role: 'main', classes: ['editor'], options: { restorePreviousState: this.state.editor.restoreEditors } },
            { id: Parts.PANEL_PART, role: 'complementary', classes: ['panel', this.state.panel.position === Position.BOTTOM ? 'bottom' : 'right'] },
            { id: Parts.STATUSBAR_PART, role: 'contentinfo', classes: ['statusbar'] }
        ].forEach(({ id, role, classes, options }) => {
            const partContainer = this.createPart(id, role, classes);
            if (!configurationService.getValue('workbench.useExperimentalGridLayout')) {
                // TODO@Ben cleanup once moved to grid
                // Insert all workbench parts at the beginning. Issue #52531
                // This is primarily for the title bar to allow overriding -webkit-app-region
                this.container.insertBefore(partContainer, this.container.lastChild);
            }
            this.getPart(id).create(partContainer, options);
        });
        // 将工作台添加至container dom渲染
        this.parent.appendChild(this.container);
    }

workbench最后调用this.layout()方法,将窗口占据整个界面,渲染完成

layout(options?: ILayoutOptions): void {
        if (!this.disposed) {
            this._dimension = getClientArea(this.parent);
            if (this.workbenchGrid instanceof Grid) {
                position(this.container, 0, 0, 0, 0, 'relative');
                size(this.container, this._dimension.width, this._dimension.height);
                // Layout the grid widget
                this.workbenchGrid.layout(this._dimension.width, this._dimension.height);
            } else {
                this.workbenchGrid.layout(options);
            }
            // Emit as event
            this._onLayout.fire(this._dimension);
        }
    }

开发调试

app.once('ready', function () {
    //启动追踪
    if (args['trace']) {
        // @ts-ignore
        const contentTracing = require('electron').contentTracing;
        const traceOptions = {
            categoryFilter: args['trace-category-filter'] || '*',
            traceOptions: args['trace-options'] || 'record-until-full,enable-sampling'
        };
        contentTracing.startRecording(traceOptions, () => onReady());
    } else {
        onReady();
    }
});

启动追踪

这里如果传入trace参数,在onReady启动之前会调用chromium的收集跟踪数据, 提供的底层的追踪工具允许我们深度了解 V8 的解析以及其他时间消耗情况,

一旦收到可以开始记录的请求,记录将会立马启动并且在子进程是异步记录听的. 当所有的子进程都收到 startRecording 请求的时候,callback 将会被调用.

categoryFilter是一个过滤器,它用来控制那些分类组应该被用来查找.过滤器应当有一个可选的 - 前缀来排除匹配的分类组.不允许同一个列表既是包含又是排斥.

contentTracing.startRecording(options, callback)

  • options Object
    • categoryFilter String
    • traceOptions String
  • callback Function

关于trace的详细介绍

结束追踪

contentTracing.stopRecording(resultFilePath, callback)

  • resultFilePath String
  • callback Function 在成功启动窗口后,程序结束性能追踪,停止对所有子进程的记录.

子进程通常缓存查找数据,并且仅仅将数据截取和发送给主进程.这有利于在通过 IPC 发送查找数据之前减小查找时的运行开销,这样做很有价值.因此,发送查找数据,我们应当异步通知所有子进程来截取任何待查找的数据.

一旦所有子进程接收到了 stopRecording 请求,将调用 callback ,并且返回一个包含查找数据的文件.

如果 resultFilePath 不为空,那么将把查找数据写入其中,否则写入一个临时文件.实际文件路径如果不为空,则将调用 callback .

debug

调试界面在菜单栏找到 Help->Toggle Developers Tools

调出Chrome开发者调试工具进行调试

🤡VS Code 插件开发文档

术语表


术语表收录出现在VS Code中独有的或是易引起歧义的词汇,不包含常见词汇,如Extension。该表按首字母排序。 该表格式:

  • 普通词汇 英文名称 术语名称

  • 单义词 [出处或参考解释链接]() 术语名称:解释

  • 多义词 [出处或参考解释链接]() 术语名称1/术语名称2:解释

  • Activation Events 激活事件:用于激活插件的VS Code事件钩子。

  • Contribution Points 发布内容配置点:package.json的一部分,用于配置插件启动命令、用户可更改的插件配置,可以理解为插件的主要配置文件。

  • Debug Adapter 调试适配器:连接真正的调试程序(或运行时)和调试界面的插件称之为调试适配器。VS Code没有原生调试程序,而是依赖【调试器插件】调用通信协议(调试适配器协议)和VS Code的调试器界面实现。

  • Extension Manifest 插件清单:VS Code自定义的pacakge.json文件,其中包含着插件的入口、配置等重要信息。

  • Extensibility 扩展性

  • Extension Host 扩展主机:与VS Code主进程隔离的插件进程,插件运行的地方,开发者可在这个进程中调用VS Code提供的各类API。

  • Language Servers 语言服务器:插件模式中使用C/S结构的的服务器端,用于高消耗的特殊插件场景,如语言解析、智能提示等。与之相对,客户端则是普通的插件,两者通过VS Code 的API进行通信。

  • Language Identifier 语言标识符:定义在发布内容配置的特定标识/名称,便于后续引用该语言配置。通常为某种编程语言的通俗名称,如JavaScript的语言标识符是【javascript】,Python的语言标识符是【python】。

预备知识

认识TypeScript-变量和类型

本节将介绍基础的TypeScript变量以及它的类型系统,它本质上和JavaScript是一样的,不过东西会更多一点,对于非js开发者来说,你可能会遇到熟悉的“枚举”、“元组”类型,了解了这点,或许能让你安心并更快地掌握TS,但是这并不意味着你就可以高枕无忧了,虽然TS扩展了JS的类型能力,但它本质上依旧是一门弱类型语言,请在书写代码时遵循社区的最佳实践并保持谨慎。

?> 本文参考社区翻译文档,详见https://www.tslang.cn/docs/handbook/basic-types.html

类型


变量声明的基础规则请自行参考JavaScript,JavaScript支持加分号和不加分号两种风格,方便起见,本章的所有示例代码都不会刻意添加分号,有关分号风格,请参阅。

类型注解

TS扩展了JS的语法格式,规则:在变量、声明的后面立即加上冒号:,如:

// 字符串注解
const XXX: string = 'string type'
// 布尔值注解
let true_or_false: boolean = false
// 函数参数类型注解
function params (value: string) {
    console.log(value) // 返回string类型
}
// 函数返回值类型注解
function returnValue (): string {
    return 'value is stirng'
}

请注意,类型注释应使用小写,而不是使用首字母大写的JavaScript的衍生类型(应使用string,而不是String

布尔值

最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean(其它语言中也一样)。

let isDone: boolean = false;

数字

和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number。 除了支持十进制和十六进制字面量,TypeScript还支持ECMAScript 2015中引入的二进制和八进制字面量。非JS开发者需要注意的是,TS和JS一样,没有区分数字类型(如Int,Long),如果你需要整数,需使用Number.parseInt()方法。

let decLiteral: number = 6;
let hexLiteral: number = 0xf00d;
let binaryLiteral: number = 0b1010;
let octalLiteral: number = 0o744;

字符串

JavaScript程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用 string表示文本数据类型。 和JavaScript一样,可以使用双引号( “)或单引号(’)表示字符串。

let name: string = "bob";
name = "smith";

你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围( ),并且以${ expr }`这种形式嵌入表达式

let name: string = `Gene`;
let age: number = 37;
let sentence: string = `Hello, my name is ${ name }.
I'll be ${ age + 1 } years old next month.`;

这与下面定义sentence的方式效果相同:

let sentence: string = "Hello, my name is " + name + ".\n\n" +
    "I'll be " + (age + 1) + " years old next month.";

数组

TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上 [],表示由此类型元素组成的一个数组:

let list: number[] = [1, 2, 3];

第二种方式是使用数组泛型,Array<元素类型>:

let list: Array<number> = [1, 2, 3];

元组 Tuple

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为 string和number类型的元组。

// 声明一个元组
let x: [string, number];
// 将其初始化
x = ['hello', 10]; // OK
// 将其错误地初始化
x = [10, 'hello']; // Error
当访问一个已知索引的元素,会得到正确的类型:
console.log(x[0].substr(1)); // OK
console.log(x[1].substr(1)); // Error, 'number'类型没有'substr'方法

枚举

enum类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

默认情况下,从0开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1开始编号:

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

或者,全部都采用手动赋值:

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:

enum Color {Red = 1, Green, Blue}
let colorName: string = Color[2];
console.log(colorName);  // 显示'Green'因为上面代码里它的值是2

Any

有时候,我们会想要为那些在编写阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any类型来标记这些变量:

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // 合法, 定义了一个布尔值

当你只知道一部分数据的类型时,any类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:

let list: any[] = [1, true, "free"];
list[1] = 100;

Void

某种程度上来说,void类型像是与any类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void:

function warnUser(): void {
    console.log("This is my warning message");
}

声明一个void类型的变量没有什么大用,因为你只能为它赋予undefined和null:

let unusable: void = undefined;

Object

object表示非原始类型,也就是除number,string,boolean,symbol,null或undefined之外的类型。

使用object类型,就可以更好的表示像Object.create这样的API。例如:

declare function create(o: object | null): void;
create({ prop: 0 }); // OK
create(null); // OK
create(42); // Error
create("string"); // Error
create(false); // Error
create(undefined); // Error

TypeScript类型表


类型 例子
基本类型
boolean x: boolean = false
number x: number = 10
string x: string = '10'
undefined x: undefined = undefined
null x: null = null
引用类型以及其他类型
object x: object = { age: '14', name: 'John' }
array x: array = [1, '2', 3.0]
function x: function = (args) => { console.log(args) }
symbol x: symbol = Symbol('id')
TypeScript 补充类型
any x: any = null
never function error (msg): never => { throw new Error(msg) }
enum enum Color {Red = 1, Green, Blue}
tuple x: [string, number] = ['name', 12]

类型断言


有时候你会遇到这样的情况,你比TypeScript更了解某个值的具体信息。

通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。它没有运行时的影响,只是在编译阶段起作用。TypeScript会假设你——程序员——已经进行了必须的检查。

类型断言有两种形式。 其一是“尖括号”语法:

let someValue: any = "this is a string";
let strLength: number = (<string>someValue).length;

另一个为as语法:

let someValue: any = "this is a string";
let strLength: number = (someValue as string).length;

!> 注意:本章全部采用了let,const关键字,以及你接下来可以见到的所有例子中,都不再使用var声明变量,为了避免var带来的副作用和影响,我们更提倡使用新的关键字。

变量声明


let声明

let的声明格式

let hello = "Hello!";
块作用域

当用let声明一个变量,它使用的是词法作用域或块作用域。 不同于使用 var声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或for循环之外是不能访问的。

function f(input: boolean) {
    let a = 100;
    if (input) {
        // 你在这里还可以访问到a
        let b = a + 1;
        return b;
    }
    // 错误: 'b'不存在
    return b;
}

这里我们定义了2个变量aba的作用域在f函数体内,而b的作用域只在if语句块里。

拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 暂时性死区。 它只是用来说明我们不能在 let语句之前访问它们,幸运的是TypeScript可以告诉我们这些信息。

a++; // illegal to use 'a' before it's declared;
let a;

注意一点,我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为ES2015,运行时会抛出一个错误;然而,目前TypeScript是不会报错的。

function foo() {
    // okay to capture 'a'
    return a;
}
// 不能在'a'被声明前调用'foo'
// 运行时会抛出错误
foo();
let a;

重定义及屏蔽

你不可以重复定义同一个变量,在使用var时,这是被允许的。

let x = 10;
let x = 20; // 错误,不能在1个作用域里多次声明`x`

以下情况,TypeScript均会报错

function f(x) {
    let x = 100; // error: interferes with parameter declaration
}
function g() {
    let x = 100;
    var x = 100; // error: can't have both declarations of 'x'
}

注意函数作用域和块作用域不同,你可以在函数作用域里嵌套块作用域,作用域之间的变量声明互不影响,同时,块作用域是允许嵌套的。

在一个嵌套作用域里引入一个新名字的行为称做屏蔽。 它是一把双刃剑,它可能会不小心地引入新问题,还可能会遮蔽掉一些错误。

function f(condition, x) {
    if (condition) {
        let x = 100;
        return x;
    }
    return x;
}
f(false, 0); // returns 0
f(true, 0);  // returns 100

const声明

const声明是声明变量的另一种方式。它们与let声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与 let相同的作用域规则,但是不能对它们重新赋值。

这很好理解,它们引用的值是不可变的。

const numLivesForCat = 9;
const kitty = {
    name: "Aurora",
    numLives: numLivesForCat,
}
// Error
kitty = {
    name: "Danielle",
    numLives: numLivesForCat
};
// all "okay"
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

除非你使用特殊的方法去避免,实际上const变量的内部状态是可修改的。

访问/设置对象的属性和方法


我们在前面已经看过很多对象的例子了,而且JavaScript的各种衍生类型都是基于Object构造出来的,所以本小节介绍的内容也同时适用数组、元组等数据类型。

点表示法

对象的名字表现为一个命名空间(namespace),它必须写在第一位——当你想访问对象内部的属性或方法时,然后是一个点.,紧接着是你想要访问的项目,标识可以是简单属性的名字(name),或者是数组属性的一个子元素,又或者是对象的方法调用。

person.age
person.interests[1]
person.bio()

创建一个对象在TypeScript中非常简单,在赋值语句右侧使用形如{}的方式就能创建对象

const name = {
  first : 'Bob',
  last : 'Smith'
}

用点表示法访问对象属性

name.firstname.last

中括号表示法

另外一种访问属性的方式是使用括号表示法(bracket notation),替代这样的代码

person.age
person.name.first

person['age']
person['name']['first']

同样,创建一个数组也非常容易

const name = ['Bob', 'Smith']

用中括号表示法访问数组元素

name[0]
name[1]
// 数组或类数组格式的数据只能用括号表示法访问元素,不可以使用`name.0`方式访问

设置对象成员

分别用点表示法和中括号表示法设置对象成员的值

person.age = 45
person['name']['last'] = 'Cratchit'

设置成员并不意味着你只能更新已经存在的属性的值,你完全可以创建新的成员,尝试以下代码:

person['eyes'] = 'hazel'
person.farewell = function() { alert("Bye everybody!") }

现在你可以测试你新创建的成员

person['eyes']
person.farewell()

变量解构


ES2015的变量解构本质来说是一种便利的语法糖

解构数组

最简单的解构莫过于数组的解构赋值了:

let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2

上面的例子等价于

first = input[0];
second = input[1];

作用于函数参数:

function f([first, second]: [number, number]) { // 注意后面部分[number, number]是typescript的类型注解
    console.log(first);
    console.log(second);
}
const input = [12, 44]
f(input);

你可以在数组里使用...语法创建剩余变量:

let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]

当然,由于是JavaScript, 你可以忽略你不关心的尾随元素:

let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1

或其它元素:

let [, second, , fourth] = [1, 2, 3, 4];

对象解构

你也可以解构对象:

let o = {
    a: "foo",
    b: 12,
    c: "bar"
};
let { a, b } = o;

你可以在对象里使用...语法创建剩余变量:

let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;

你也可以给属性以不同的名字:

let { a: newName1, b: newName2 } = o;

这里的语法开始有些混乱。 你可以将 a: newName1 读做讲 “a” 取出作为 “newName1”。 方向是从左到右,好像你写成了以下样子:

let newName1 = o.a;
let newName2 = o.b;

令你困惑的可能是=这里的冒号不是指示类型的,我们前面说,以冒号后跟的是类型注解。 如果你想指定它的类型,仍然需要在其后写上完整的模式。

// 正确
let {a, b}: {a: string, b: number} = o;
// 错误
let {a: n1, b: n2}: {n1: string, n2: number} = o;

默认值

默认值可以让你在属性为 undefined 时使用缺省值:

function keepWholeObject(wholeObject: { a: string, b?: number }) {
    let { a, b = 1001 } = wholeObject;
}

现在,即使 bundefined , keepWholeObject 函数的变量 wholeObject 的属性 a 和 b 都会有值。

函数声明

解构也能用于函数声明。 看以下简单的情况:


type C = { a: string, b?: number }
// 普通写法
function f(C) {
    // ...
}
// 解构
function f({ a, b }: C): void {
    // ...
}

变量展开

展开操作符正与解构相反。 它允许你将一个数组展开为另一个数组,或将一个对象展开为另一个对象。 例如:

let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

这会令bothPlus的值为[0, 1, 2, 3, 4, 5]。 展开操作创建了 first和second的一份浅拷贝。 它们不会被展开操作所改变。

你还可以展开对象:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };

search的值为{ food: "rich", price: "$$", ambiance: "noisy" }。 对象的展开比数组的展开要复杂的多。 像数组展开一样,它是从左至右进行处理,但结果仍为对象。 这就意味着出现在展开对象后面的属性会覆盖前面的属性。 因此,如果我们修改上面的例子,在结尾处进行展开的话:

let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };

那么,defaults里的food属性会重写food: "rich",在这里这并不是我们想要的结果。

认识Typescript-类

传统的JavaScript程序使用函数和基于原型的继承来创建可重用的组件。Typescript则在此基础上加入了语法糖

了解Javascript的基础内容请参考对象原型

声明类

我们首先来看一个例子

class Greeter {
    greeting: string;
    constructor(message: string) {
        this.greeting = message;
    }
    greet() {
        return "Hello, " + this.greeting;
    }
}
let greeter = new Greeter("world");

如果你使用过C#或Java,你会对这种语法非常熟悉。 我们声明一个 Greeter类。这个类有3个成员:一个叫做 greeting的属性,一个构造函数和一个 greet方法。

你会注意到,我们在引用任何一个类成员的时候都用了 this。 它表示我们访问的是类的成员。

最后一行,我们使用 new构造了 Greeter类的一个实例。 它会调用之前定义的构造函数,创建一个 Greeter类型的新对象,并执行构造函数初始化它。

继承

在TypeScript里,我们可以使用常用的面向对象模式。 基于类的程序设计中一种最基本的模式是允许使用继承来扩展现有的类。

看下面的例子:

class Animal {
    move(distance: number = 0) {
        console.log(`Animal moved ${distance}m.`);
    }
}
class Dog extends Animal {
    bark() {
        console.log('Woof! Woof!');
    }
}
const dog = new Dog();
dog.bark();
dog.move(10);
dog.bark();

这个例子展示了最基本的继承:类从基类中继承了属性和方法。这里,Dog是一个 派生类,通过 extends关键字使它从 Animal 基类中派生出来。 派生类通常被称作 子类,基类通常被称作 超类或父类。

因为 Dog继承了 Animal的功能,因此我们可以创建一个 Dog的实例,它能够 bark()和 move()。

下面我们来看个更加复杂的例子。

class Animal {
    name: string;
    constructor(theName: string) { this.name = theName; }
    move(distance: number = 0) {
        console.log(`${this.name} moved ${distance}m.`);
    }
}
class Snake extends Animal {
    constructor(name: string) { super(name); }
    move(distance = 5) {
        console.log("Slithering...");
        super.move(distance);
    }
}
class Horse extends Animal {
    constructor(name: string) { super(name); }
    move(distance = 45) {
        console.log("Galloping...");
        super.move(distance);
    }
}
let sam = new Snake("Sammy the Python");
let tom: Animal = new Horse("Tommy the Palomino");
sam.move();
tom.move(34);

与前一个例子的不同点是,这次两个派生类包含了一个构造函数,它必须调用 super(),它会执行基类的构造函数。 而且,在构造函数里访问 this的属性之前,我们 必须要调用 super(),这个是TypeScript强制规定的。

修饰符

public

如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用 public来做修饰;例如,C#要求必须明确地使用 public指定成员是可见的。 在TypeScript里,成员都默认为 public。在上面的例子里,我们可以自由的访问程序里定义的成员。

private

当成员被标记成 private时,它就不能在声明它的类的外部访问。比如

class Animal {
    private name: string;
    constructor(theName: string) { this.name = theName; }
}
new Animal("Cat").name; // 错误: 'name' 是私有的.

protected

protected修饰符与 private修饰符的行为很相似,但有一点不同, protected成员在派生类中仍然可以访问。

class Person {
    protected name: string;
    constructor(name: string) { this.name = name; }
}
class Employee extends Person {
    private department: string;
    constructor(name: string, department: string) {
        super(name)
        this.department = department;
    }
    public getElevatorPitch() {
        return `Hello, my name is ${this.name} and I work in ${this.department}.`;
    }
}
let howard = new Employee("Howard", "Sales");
console.log(howard.getElevatorPitch());
console.log(howard.name); // 错误

注意,我们不能在 Person类外使用 name,但是我们仍然可以通过 Employee类的实例方法访问,因为 Employee是由 Person派生而来的。

readonly

你可以使用 readonly关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。

class Octopus {
    readonly name: string;
    readonly numberOfLegs: number = 8;
    constructor (theName: string) {
        this.name = theName;
    }
}
let dad = new Octopus("Man with the 8 strong legs");
dad.name = "Man with the 3-piece suit"; // 错误! name 是只读的.

get/set

TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。

下面来看如何把一个简单的类改写成使用 getset。 首先,我们从一个没有使用存取器的例子开始。

class Employee {
    fullName: string;
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    console.log(employee.fullName);
}

下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改员工信息。 我们把对 fullName的直接访问改成了可以检查密码的 set方法。 我们也加了一个 get方法,让上面的例子仍然可以工作。

let passcode = "secret passcode";
class Employee {
    private _fullName: string;
    get fullName(): string {
        return this._fullName;
    }
    set fullName(newName: string) {
        if (passcode && passcode == "secret passcode") {
            this._fullName = newName;
        }
        else {
            console.log("Error: Unauthorized update of employee!");
        }
    }
}
let employee = new Employee();
employee.fullName = "Bob Smith";
if (employee.fullName) {
    alert(employee.fullName);
}

static

到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。

class Router {
    static baseRoute = '/basePath';
    calculateRoute(path: string) {
        return this.baseRoute + this.commonPrefix  + path;
    }
    constructor (public commonPrefix: number) { }
}
let route1 = new Router('/api');  // 一级路径为/api
let route2 = new Router('/page');  // 一级路径为/page
console.log(route1.calculateRoute('/main');  // 最终路径/basePath/api/main
console.log(route2.calculateRoute('/user');  // 最终路径/basePath/page/user

abstract

抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。 abstract关键字是用于定义抽象类和在抽象类内部定义抽象方法。

abstract class Animal {
    abstract makeSound(): void;
    move(): void {
        console.log('roaming the earch...');
    }
}

认识TypeScript-类型接口和命名空间

TypeScript的核心原则之一是对数据的结构进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约

接口


接口初探

下面通过一个简单示例来观察接口是如何工作的:

function printLabel(labelledObj: { label: string }) {
  console.log(labelledObj.label);
}
let myObj = { size: 10, label: "Size 10 Object" };
printLabel(myObj);

类型检查器会检查printLabel的调用。 printLabel有一个参数,并要求这个对象参数有一个名为label类型为string的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。 然而,有些时候TypeScript却并不会这么宽松,我们下面会稍做讲解。

下面我们重写上面的例子,这次使用接口来描述:必须包含一个label属性且类型为string

interface LabelledValue {
  label: string;
}
function printLabel(labelledObj: LabelledValue) {
  console.log(labelledObj.label);
}
let myObj = {size: 10, label: "Size 10 Object"};
printLabel(myObj);

LabelledValue接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label属性且类型为string的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给 printLabel的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。

还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。

可选属性

接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。

下面是应用了“option bags”的例子:

interface SquareConfig {
  color?: string;
  width?: number;
}
function createSquare(config: SquareConfig): {color: string; area: number} {
  let newSquare = {color: "white", area: 100};
  if (config.color) {
    newSquare.color = config.color;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}
let mySquare = createSquare({color: "black"});

带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?符号。

可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。 比如,我们故意将 createSquare里的color属性名拼错,就会得到一个错误提示:

interface SquareConfig {
  color?: string;
  width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
  let newSquare = {color: "white", area: 100};
  if (config.clor) {
    // Error: Property 'clor' does not exist on type 'SquareConfig'
    newSquare.color = config.clor;
  }
  if (config.width) {
    newSquare.area = config.width * config.width;
  }
  return newSquare;
}
let mySquare = createSquare({color: "black"});

只读属性

一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly来指定只读属性:

interface Point {
    readonly x: number;
    readonly y: number;
}

你可以通过赋值一个对象字面量来构造一个Point。 赋值后, x和y再也不能被改变了。

let p1: Point = { x: 10, y: 20 };
p1.x = 5; // error!

TypeScript具有ReadonlyArray<T>类型,它与Array<T>相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:

let a: number[] = [1, 2, 3, 4];
let ro: ReadonlyArray<number> = a;
ro[0] = 12; // error!
ro.push(5); // error!
ro.length = 100; // error!
a = ro; // error!

上面代码的最后一行,可以看到就算把整个ReadonlyArray赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:

a = ro as number[];

readonly vs const

最简单判断该用readonly还是const的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用 const,若做为属性则使用readonly。

额外的属性检查

我们在第一个例子里使用了接口,TypeScript让我们传入{ size: number; label: string; }到仅期望得到{ label: string; }的函数里。 我们已经学过了可选属性,并且知道他们在“option bags”模式里很有用。

然而,天真地将这两者结合的话就会像在JavaScript里那样搬起石头砸自己的脚。 比如,拿createSquare例子来说:

interface SquareConfig {
    color?: string;
    width?: number;
}
function createSquare(config: SquareConfig): { color: string; area: number } {
    // ...
}
let mySquare = createSquare({ colour: "red", width: 100 });

注意传入createSquare的参数拼写为colour而不是color。 在JavaScript里,这会导致静默失败。

你可能会争辩这个程序已经正确地类型化了,因为width属性是兼容的,不存在color属性,而且额外的colour属性是无意义的。

然而,TypeScript会认为这段代码可能存在bug。 对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。

// error: 'colour' not expected in type 'SquareConfig'
let mySquare = createSquare({ colour: "red", width: 100 });

绕开这些检查非常简单。 最简便的方法是使用类型断言:

let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);

然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果 SquareConfig带有上面定义的类型的colorwidth属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:

interface SquareConfig {
    color?: string;
    width?: number;
    [propName: string]: any;
}

我们稍后会讲到索引签名,但在这我们要表示的是SquareConfig可以有任意数量的属性,并且只要它们不是colorwidth,那么就无所谓它们的类型是什么。

还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量: 因为 squareOptions不会经过额外属性检查,所以编译器不会报错。

let squareOptions = { colour: "red", width: 100 };
let mySquare = createSquare(squareOptions);

要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的bug。 就是说你遇到了额外类型检查出的错误,比如“option bags”,你应该去审查一下你的类型声明。 在这里,如果支持传入 colorcolour属性到createSquare,你应该修改SquareConfig定义来体现出这一点。

函数类型

接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。

为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。

interface SearchFunc {
  (source: string, subString: string): boolean;
}

这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。

let mySearch: SearchFunc;
mySearch = function(source: string, subString: string) {
  let result = source.search(subString);
  return result > -1;
}

对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 比如,我们使用下面的代码重写上面的例子:

let mySearch: SearchFunc;
mySearch = function(src: string, sub: string): boolean {
  let result = src.search(sub);
  return result > -1;
}

函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,TypeScript的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是 false和true)。 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与 SearchFunc接口中的定义不匹配。

let mySearch: SearchFunc;
mySearch = function(src, sub) {
    let result = src.search(sub);
    return result > -1;
}

可索引的类型

与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]ageMap["daniel"]。 可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。 让我们看一个例子:

interface StringArray {
  [index: number]: string;
}
let myArray: StringArray;
myArray = ["Bob", "Fred"];
let myStr: string = myArray[0];

上面例子里,我们定义了StringArray接口,它具有索引签名。 这个索引签名表示了当用 number去索引StringArray时会得到string类型的返回值。

TypeScript支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number来索引时,JavaScript会将它转换成string然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用”100”(一个string)去索引,因此两者需要保持一致。

class Animal {
    name: string;
}
class Dog extends Animal {
    breed: string;
}
// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal!
interface NotOkay {
    [x: number]: Animal;
    [x: string]: Dog;
}

字符串索引签名能够很好的描述dictionary模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.propertyobj["property"]两种形式都可以。 下面的例子里, name的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:

interface NumberDictionary {
  [index: string]: number;
  length: number;    // 可以,length是number类型
  name: string       // 错误,`name`的类型与索引类型返回值的类型不匹配
}

最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:

interface ReadonlyStringArray {
    readonly [index: number]: string;
}
let myArray: ReadonlyStringArray = ["Alice", "Bob"];
myArray[2] = "Mallory"; // error!

你不能设置myArray[2],因为索引签名是只读的。

类类型

与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确或强制一个类去符合某种契约。

interface ClockInterface {
    currentTime: Date;
}
class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

你也可以在接口中描述一个方法,在类里实现它,如同下面的setTime方法一样:

interface ClockInterface {
    currentTime: Date;
    setTime(d: Date);
}
class Clock implements ClockInterface {
    currentTime: Date;
    setTime(d: Date) {
        this.currentTime = d;
    }
    constructor(h: number, m: number) { }
}

接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。

类的静态部分和实例部分

当你在类上面使用接口时,你需要注意一个是由两部分构成的:类的静态部分和实例部分。如果你的接口上面声明了构造器签名,然后将这个接口应用在类上,你会看到报错:

interface ClockConstructor {
    new (hour: number, minute: number);
}
class Clock implements ClockConstructor {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

这是因为一个类实现某个接口时,接口只检查的实例部分,而constructor构造器方法是类的静态部分,因此它不包含在类型检查之内。

所以,我们其实只要直接操作类的静态部分就可以了。在下述例子中,我们定义2个接口,一个ClockConstructor给构造器使用,一个ClockInterface供实例方法使用。方便起见,我们再定义一个构造器函数createClock

interface ClockConstructor {
    new (hour: number, minute: number): ClockInterface;
}
interface ClockInterface {
    tick(): void;
}
function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface {
    return new ctor(hour, minute);
}
class DigitalClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("beep beep");
    }
}
class AnalogClock implements ClockInterface {
    constructor(h: number, m: number) { }
    tick() {
        console.log("tick tock");
    }
}

因为createClock的第一个参数是ClockConstructor类型,在createClock(AnalogClock, 7, 32)里,会检查AnalogClock是否符合构造函数签名。

接口继承

和类一样,接口也可以相互继承。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。

interface Shape {
    color: string;
}
interface Square extends Shape {
    sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;

一个接口可以继承多个接口,创建出多个接口的合成接口。

interface Shape {
    color: string;
}
interface PenStroke {
    penWidth: number;
}
interface Square extends Shape, PenStroke {
    sideLength: number;
}
let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
square.penWidth = 5.0;

混合类型

先前我们提过,接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。

一个例子就是,一个对象可以同时做为函数和对象使用,并带有额外的属性。

interface Counter {
    (start: number): string;
    interval: number;
    reset(): void;
}
function getCounter(): Counter {
    let counter = <Counter>function (start: number) { };
    counter.interval = 123;
    counter.reset = function () { };
    return counter;
}
let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;

命名空间


第一步

我们先来写一段程序并将在整篇文章中都使用这个例子。 我们定义几个简单的字符串验证器,假设你会使用它们来验证表单里的用户输入或验证外部数据。

所有的验证器都放在一个文件里:

interface StringValidator {
    isAcceptable(s: string): boolean;
}
let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;
class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
        return lettersRegexp.test(s);
    }
}
class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
// 一个使用例子
let strings = ["Hello", "98052", "101"];
// 可以使用的校验器
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
// 每个strings是否都能找到对应的校验器
for (let s of strings) {
    for (let name in validators) {
        let isMatch = validators[name].isAcceptable(s);
        console.log(`'${ s }' ${ isMatch ? "matches" : "does not match" } '${ name }'.`);
    }
}

命名空间

随着更多验证器的加入,我们需要一种手段来组织代码,以便于在记录它们类型的同时还不用担心与其它对象产生命名冲突。 因此,我们把验证器包裹到一个命名空间内,而不是把它们放在全局命名空间下。

下面的例子里,把所有与验证器相关的类型都放到一个叫做Validation的命名空间里。 因为我们想让这些接口和类在命名空间之外也是可访问的,所以需要使用export。 相反的,变量 lettersRegexpnumberRegexp是实现的细节,不需要导出,因此它们在命名空间外是不能访问的。 在文件末尾的测试代码里,由于是在命名空间之外访问,因此需要限定类型的名称,比如 Validation.LettersOnlyValidator

使用命名空间的验证器:

namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }
    const lettersRegexp = /^[A-Za-z]+$/;
    const numberRegexp = /^[0-9]+$/;
    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }
    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}
// 一个例子
let strings = ["Hello", "98052", "101"];
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
for (let s of strings) {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
}

分离文件

当应用变得越来越大时,我们需要将代码分离到不同的文件中以便于维护。

现在,我们把Validation命名空间分割成多个文件。 尽管是不同的文件,它们仍是同一个命名空间,并且在使用的时候就如同它们在一个文件中定义的一样。 因为不同文件之间存在依赖关系,所以我们加入了引用标签来告诉编译器文件之间的关联。 我们的测试代码保持不变。

Validation.ts

namespace Validation {
    export interface StringValidator {
        isAcceptable(s: string): boolean;
    }
}

LettersOnlyValidator.ts

/// <reference path="Validation.ts" />
namespace Validation {
    const lettersRegexp = /^[A-Za-z]+$/;
    export class LettersOnlyValidator implements StringValidator {
        isAcceptable(s: string) {
            return lettersRegexp.test(s);
        }
    }
}

ZipCodeValidator.ts

/// <reference path="Validation.ts" />
namespace Validation {
    const numberRegexp = /^[0-9]+$/;
    export class ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

Test.ts

/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />
// Some samples to try
let strings = ["Hello", "98052", "101"];
// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// Show whether each string passed each validator
for (let s of strings) {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
}

当涉及到多文件时,我们必须确保所有编译后的代码都被加载了。 我们有两种方式。

第一种方式,把所有的输入文件编译为一个输出文件,需要使用—outFile标记:

tsc --outFile sample.js Test.ts

编译器会根据源码里的引用标签自动地对输出进行排序。你也可以单独地指定每个文件。

tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

第二种方式,我们可以编译每一个文件(默认方式),那么每个源文件都会对应生成一个JavaScript文件。 然后,在页面上通过 <script>标签把所有生成的JavaScript文件按正确的顺序引进来,比如:

<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidator.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />

认识Typescript-泛型

软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。

在像C#和Java这样的语言中,可以使用泛型来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。

泛型之Hello World


下面来创建第一个使用泛型的例子:identity函数。 这个函数会返回任何传入它的值。 你可以把这个函数当成是 echo命令。

不用泛型的话,这个函数可能是下面这样:

function identity(arg: number): number {
    return arg;
}

或者,我们使用any类型来定义函数:

function identity(arg: any): any {
    return arg;
}

使用any类型会导致这个函数可以接收任何类型的arg参数,这样就丢失了一些信息:传入的类型与返回的类型应该是相同的。如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。

因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了类型变量,它是一种特殊的变量,只用于表示类型而不是值

function identity<T>(arg: T): T {
    return arg;
}

我们给identity添加了类型变量TT帮助我们捕获用户传入的类型(比如:number),之后我们就可以使用这个类型。 之后我们再次使用了T当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。

我们把这个版本的identity函数叫做泛型,因为它可以适用于多个类型。 不同于使用any,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。

我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:

let output = identity<string>("myString");  // type of output will be 'string'

这里我们明确的指定了Tstring类型,并做为一个参数传给函数,使用了<>括起来而不是()

第二种方法更普遍。利用了类型推论 — 即编译器会根据传入的参数自动地帮助我们确定T的类型:

let output = identity("myString");  // type of output will be 'string'

注意我们没必要使用尖括号(<>)来明确地传入类型;编译器可以查看myString的值,然后把T设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。

使用泛型变量


使用泛型创建像identity这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。

看下之前identity例子:

function identity<T>(arg: T): T {
    return arg;
}

如果我们想同时打印出arg的长度。 我们很可能会这样做:

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

如果这么做,编译器会报错说我们使用了arg.length属性,但是没有地方指明arg具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length属性的。

现在假设我们想操作T类型的数组而不直接是T。由于我们操作的是数组,所以.length属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:

function loggingIdentity<T>(arg: T[]): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

你可以这样理解loggingIdentity的类型:泛型函数loggingIdentity,接收类型参数T和参数arg,它是个元素类型是T的数组,并返回元素类型是T的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时 T的的类型为number。 这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。

我们也可以这样实现上面的例子:

function loggingIdentity<T>(arg: Array<T>): Array<T> {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

使用过其它语言的话,你可能对这种语法已经很熟悉了。 在下一节,会介绍如何创建自定义泛型像 Array一样。

泛型类型


上一节,我们创建了identity通用函数,可以适用于不同的类型。 在这节,我们研究一下函数本身的类型,以及如何创建泛型接口。

泛型函数的类型与非泛型函数的类型没什么不同,只是有一个类型参数在最前面,像函数声明一样:

function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: <T>(arg: T) => T = identity;

我们也可以使用不同的泛型参数名,只要在数量上和使用方式上能对应上就可以。

function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: <U>(arg: U) => U = identity;

我们还可以使用带有调用签名的对象字面量来定义泛型函数:

function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: {<T>(arg: T): T} = identity;

这引导我们去写第一个泛型接口了。 我们把上面例子里的对象字面量拿出来做为一个接口:

interface GenericIdentityFn {
    <T>(arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn = identity;

一个相似的例子,我们可能想把泛型参数当作整个接口的一个参数。 这样我们就能清楚的知道使用的具体是哪个泛型类型(比如: Dictionary<string>而不只是Dictionary)。 这样接口里的其它成员也能知道这个参数的类型了。

interface GenericIdentityFn<T> {
    (arg: T): T;
}
function identity<T>(arg: T): T {
    return arg;
}
let myIdentity: GenericIdentityFn<number> = identity;

注意,我们的示例做了少许改动。 不再描述泛型函数,而是把非泛型函数签名作为泛型类型一部分。 当我们使用 GenericIdentityFn 的时候,还得传入一个类型参数来指定泛型类型(这里是:number),锁定了之后代码里使用的类型。 对于描述哪部分类型属于泛型部分来说,理解何时把参数放在调用签名里和何时放在接口上是很有帮助的。

除了泛型接口,我们还可以创建泛型类。 注意,无法创建泛型枚举和泛型命名空间。

泛型类


class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

GenericNumber类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number类型。 也可以使用字符串或其它更复杂的类型。

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = "";
stringNumeric.add = function(x, y) { return x + y; };
console.log(stringNumeric.add(stringNumeric.zeroValue, "test"));

与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。

泛型约束


你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在 loggingIdentity 例子中,我们想访问arglength属性,但是编译器并不能证明每种类型都有length属性,所以就报错了。

function loggingIdentity<T>(arg: T): T {
    console.log(arg.length);  // Error: T doesn't have .length
    return arg;
}

相比于操作any所有类型,我们想要限制函数去处理任意带有.length属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。

为此,我们定义一个接口来描述约束条件。 创建一个包含 .length属性的接口,使用这个接口和extends关键字来实现约束:

interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // Now we know it has a .length property, so no more error
    return arg;
}

现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:

loggingIdentity(3);  // Error, number doesn't have a .length property

我们需要传入符合约束类型的值,必须包含必须的属性:

loggingIdentity({length: 10, value: 3});

在泛型约束中使用类型参数

你可以声明一个类型参数,且它被另一个类型参数所约束。 比如,现在我们想要用属性名从对象里获取这个属性。 并且我们想要确保这个属性存在于对象 obj上,因此我们需要在这两个类型之间使用约束。

function getProperty(obj: T, key: K) {
    return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a"); // okay
getProperty(x, "m"); // error: Argument of type 'm' isn't assignable to 'a' | 'b' | 'c' | 'd'.

认识Typescript-声明文件

当你在使用VS Code编写TypeScript文件时,你不太可能在每个文件上都标明interface字样的接口,一旦某个文件的方法或者变量比较多,你的代码可能就会被非常多的类型注解占据,在大型项目或者是你生成的库文件中为了保持代码的整洁,你就可能需要声明文件的帮助。

创建一个声明文件


声明文件以.d.ts结尾,与TypeScript文件.ts稍有区别。当你的项目目录中出现了.d.ts结尾的文件时,所有与之关联的规则都会自动应用。声明文件支持我们前面介绍的各类语法,但是你得在语法前面加上declare

全局变量

文档

全局变量foo包含了存在组件总数。

代码

console.log("Half the number of widgets is " + (foo / 2));

声明

使用declare var声明变量。 如果变量是只读的,那么可以使用 declare const。 你还可以使用 declare let如果变量拥有块级作用域。

/** 组件总数 */
declare var foo: number;

全局函数

文档

用一个字符串参数调用greet函数向用户显示一条欢迎信息。

代码

greet("hello, world");

声明

使用declare function声明函数。

declare function greet(greeting: string): void;

带属性的对象

文档

全局变量myLib包含一个makeGreeting函数, 还有一个属性 numberOfGreetings指示目前为止欢迎数量。

代码

let result = myLib.makeGreeting("hello, world");
console.log("The computed greeting is:" + result);
let count = myLib.numberOfGreetings;

声明

使用declare namespace描述用点表示法访问的类型或值。

declare namespace myLib {
    function makeGreeting(s: string): string;
    let numberOfGreetings: number;
}

函数重载

文档

getWidget函数接收一个数字,返回一个组件,或接收一个字符串并返回一个组件数组。

代码

let x: Widget = getWidget(43);
let arr: Widget[] = getWidget("all of them");

声明

declare function getWidget(n: number): Widget;
declare function getWidget(s: string): Widget[];

可重用类型(接口)

文档

当指定一个欢迎词时,你必须传入一个GreetingSettings对象。 这个对象具有以下几个属性:

1- greeting:必需的字符串

2- duration: 可选的时长(毫秒表示)

3- color: 可选字符串,比如‘#ff00ff’

代码

greet({
  greeting: "hello world",
  duration: 4000
});

声明

使用interface定义一个带有属性的类型。

interface GreetingSettings {
  greeting: string;
  duration?: number;
  color?: string;
}
declare function greet(setting: GreetingSettings): void;

可重用类型(类型别名)

文档

在任何需要欢迎词的地方,你可以提供一个string,一个返回string的函数或一个Greeter实例。

代码

function getGreeting() {
    return "howdy";
}
class MyGreeter extends Greeter { }
greet("hello");
greet(getGreeting);
greet(new MyGreeter());

声明

你可以使用类型别名来定义类型的短名:

type GreetingLike = string | (() => string) | MyGreeter;
declare function greet(g: GreetingLike): void;

组织类型

文档

greeter对象能够记录到文件或显示一个警告。 你可以为 .log(…)提供LogOptions和为.alert(…)提供选项。

代码

const g = new Greeter("Hello");
g.log({ verbose: true });
g.alert({ modal: false, title: "Current Greeting" });

声明

使用命名空间组织类型。

declare namespace GreetingLib {
    interface LogOptions {
        verbose?: boolean;
    }
    interface AlertOptions {
        modal: boolean;
        title?: string;
        color?: string;
    }
}

你也可以在一个声明中创建嵌套的命名空间:

declare namespace GreetingLib.Options {
    // Refer to via GreetingLib.Options.Log
    interface Log {
        verbose?: boolean;
    }
    interface Alert {
        modal: boolean;
        title?: string;
        color?: string;
    }
}

文档

你可以通过实例化Greeter对象来创建欢迎词,或者继承Greeter对象来自定义欢迎词。

代码

const myGreeter = new Greeter("hello, world");
myGreeter.greeting = "howdy";
myGreeter.showGreeting();
class SpecialGreeter extends Greeter {
    constructor() {
        super("Very special greetings");
    }
}

声明

使用declare class描述一个类或像类一样的对象。 类可以有属性和方法,就和构造函数一样。

declare class Greeter {
    constructor(greeting: string);
    greeting: string;
    showGreeting(): void;
}

插件 API

VS Code内置了扩展能力,在插件API加持之下,VS Code几乎每一个部分都可以自定义或者加强。而且,VS Code中的很多核心功能已编译为插件,它们都共用了一套插件API。

本文档将介绍:

  • 如何构建、运行、调试、测试和发布插件
  • 如何利用好VS Code丰富的插件API
  • 代码示例和各个指南的位置,方便你快速入门。如果你只是想看看已经发布的插件,可移步至VS Code插件市场

插件能做什么?


下面我们看看使用插件API能做到些什么:

  • 改变VS Code的颜色和图标主题——主题
  • 在UI中添加自定义组件和视图——扩展工作台
  • 创建Webview,使用HTML/CSS/JS显示自定义网页——Webview指南
  • 支持新的编程语言——语言插件概览
  • 支持特定运行时的调试——调试插件指南

如果你想大概浏览一下所有的插件API,请参阅插件功能概述。插件指南列出了各种插件API使用的示例代码和指南。

如何构建插件?


想要做出一个好插件需要花费不少精力,我们来看看这个教程的每个章节能为你做点什么:

  • 第一步 Hello World示例会教你贯穿于制作插件时的基本概念
  • 开发插件 包含了各类插件开发更深的主题,比如发布和测试插件
  • 插件功能 将VS Code庞杂的API拆解成了几个小分类,帮你掌握到每个主题下的开发细节
  • 插件指南 包括指南和代码实例,详细介绍特定API的使用场景
  • 语言插件 通过代码和指南阐述如何添加编程语言支持
  • 进阶主题 解释了插件主机和使用不稳定的API等更深层级的概念

寻求帮助


如果你在开发中遇到了问题,请尝试:

  • Stack Overflow:其中有将近12k个打了visual-studio-code标签的问题,而且半数以上都已经有了答案,搜索你遇到的问题,提问,或者帮助其他人解决VS Code中遇到的问题。
  • Gitter频道VS Code Dev Slack:插件开发人员的公共聊天室,VS Code项目组成员偶尔也会出现。

你若对本文档有任何建议,请在Microsoft/vscode-docs中创建issue。如果你的插件问题无法解决,或者对VS Code插件API有任何建议,请在Microsoft/vscode中新建issue。

第一步

你的第一个插件

在本小节中,我们会教你一些基础概念,请先安装好Node.jsGit,然后安装YeomanVS Code Extension Generator

npm install -g yo generator-code

这个脚手架会生成一个可以立马开发的项目。运行生成器,然后填好下列字段:

yo code
# ? What type of extension do you want to create? New Extension (TypeScript)
# ? What's the name of your extension? HelloWorld
### Press <Enter> to choose default for all options below ###
# ? What's the identifier of your extension? helloworld
# ? What's the description of your extension? LEAVE BLANK
# ? Enable stricter TypeScript checking in 'tsconfig.json'? Yes
# ? Setup linting using 'tslint'? Yes
# ? Initialize a git repository? Yes
# ? Which package manager to use? npm
code ./helloworld

完成后进入 VS Code,按下F5,你会立即看到一个插件发开主机窗口,其中就运行着插件。

在命令面板(Ctrl+Shift+P)中输入Hello World命令。

如果你看到了Hello World提示弹窗,恭喜你成功了!

开发插件


现在让我们稍稍改动一下弹窗显示的内容:

  • 将项目文件extension.ts中的Hello World改为Hello VS Code
  • 重新加载开发窗口
  • 再次运行Hello World命令

请浏览你的项目目录和代码,然后进行下面的小练习:

  • 为命令面板中的Hello World换一个名字
  • 配置一个新的命令:打开一个提示弹窗,显示当前时间
  • 用显示警告信息的VS Code API替换原本的vscode.window.showInformationMessage

调试插件


VS Code 内置的调试功能已经非常方便了,在代码序号的左侧空白处点击一下,VS Code 就会设下断点,进入调试模式后将鼠标悬停于变量上显示变量值,或是在调试侧边栏中检查变量值,此时,你还可以用调试控制台直接对表达式求值。

解析插件结构

上一节中,你已经能够自己创建一个基础的插件了,但是在面纱之下,它究竟是怎么运作的呢?

Hello World插件包含了3个部分:

  • 注册onCommand 激活事件: onCommand:extension.helloWorld,所以用户可以在输入Hello World命令后激活插件。
  • 使用contributes.commands 发布内容配置,绑定一个命令ID extension.helloWorld,然后 Hello World命令就可以在命令面板中使用了。
  • 使用commands.registerCommand VS Code API 将一个函数绑定到你注册的命令IDextension.helloWorld上。

理解下面三个关键概念你才能作出一个基本的插件:

  • 激活事件: 插件激活的时机。
  • 发布内容配置: VS Code扩展了 package.json 插件清单的字段以便于开发插件。
  • VS Code API: 你的插件代码中需要调用的一系列JavaScript API。

大体上,你的插件就是通过组合发布内容配置和VS Code API扩展VS Code的功能。你能在插件功能概述主题中找到合适你插件的配置点和VS Code API。

好了,现在让我们自己瞧一瞧Hello World示例的源码部分,以及我们上面提到的3个概念是如何应用其中的。

插件目录结构


.
├── .vscode
│   ├── launch.json     // 插件加载和调试的配置
│   └── tasks.json      // 配置TypeScript编译任务
├── .gitignore          // 忽略构建输出和node_modules文件
├── README.md           // 一个友好的插件文档
├── src
│   └── extension.ts    // 插件源代码
├── package.json        // 插件配置清单
├── tsconfig.json       // TypeScript配置

下面的几个文件超出了本节讨论的范围,你可以自行前往相应的章节挖掘详细内容:

  • launch.json 用于配置VS Code 调试
  • tasks.json 用于定义VS Code 任务
  • tsconfig.json 参阅TypeScript Handbook

现在,让我们把精力集中在这个插件的关键部分——package.jsonextensions.ts

插件清单

每个VS Code插件都必须包含一个package.json,它就是插件的配置清单。package.json混合了Node.js字段,如:scriptsdependencies,还加入了一些VS Code独有的字段,如:publisheractivationEventscontributes等。关于这些VS Code字段说明都在插件清单参考中可以找到。我们在本节介绍一些非常重要的字段:

  • namepublisher: VS Code 使用<publisher>.<name>作为一个插件的ID。你可以这么理解,Hello World 例子的 ID 就是vscode-samples.helloworld-sample。VS Code 使用 ID 来区分各个不同的插件。
  • main: 插件的主入口。
  • activationEventscontributes: 激活事件 and 发布内容配置。
  • engines.vscode: 描述了这个插件依赖的最低VS Code API版本。
  • postinstall 脚本: 如果你的engines.vscode声明的是1.25版的VS Code API,那它就会按照这个声明去安装目标版本。一旦vscode.d.ts文件存在于node_modules/vscode/vscode.d.ts,IntelliSense就会开始运作,你就可以对所有VS Code API进行定义跳转或者语法检查了。
{
    "name": "helloworld-sample",
    "displayName": "helloworld-sample",
    "description": "HelloWorld example for VS Code",
    "version": "0.0.1",
    "publisher": "vscode-samples",
    "repository": "https://github.com/Microsoft/vscode-extension-samples/helloworld-sample",
    "engines": {
        "vscode": "^1.25.0"
    },
    "categories": ["Other"],
    "activationEvents": ["onCommand:extension.helloWorld"],
    "main": "./out/extension.js",
    "contributes": {
        "commands": [
            {
                "command": "extension.helloWorld",
                "title": "Hello World"
            }
        ]
    },
    "scripts": {
        "vscode:prepublish": "npm run compile",
        "compile": "tsc -p ./",
        "watch": "tsc -watch -p ./",
        "postinstall": "node ./node_modules/vscode/bin/install"
    },
    "devDependencies": {
        "@types/node": "^8.10.25",
        "tslint": "^5.11.0",
        "typescript": "^2.6.1",
        "vscode": "^1.1.22"
    }
}

插件入口文件


插件入口文件会导出两个函数,activatedeactivate,你注册的激活事件被触发之时执行activatedeactivate则提供了插件关闭前执行清理工作的机会。

vscode模块包含了一个位于node ./node_modules/vscode/bin/install的脚本,这个脚本会拉取package.jsonengines.vscode字段定义的VS Code API。这个脚本执行过后,你就得到了智能代码提示,定义跳转等TS特性了。

// 'vscode'模块包含了VS Code extensibility API
// 按下述方式导入这个模块
import * as vscode from 'vscode';
// 一旦你的插件激活,vscode会立刻调用下述方法
export function activate(context: vscode.ExtensionContext) {
    // 用console输出诊断信息(console.log)和错误(console.error)
    // 下面的代码只会在你的插件激活时执行一次
    console.log('Congratulations, your extension "my-first-extension" is now active!');
    // 入口命令已经在package.json文件中定义好了,现在调用registerCommand方法
    // registerCommand中的参数必须与package.json中的command保持一致
    let disposable = vscode.commands.registerCommand('extension.sayHello', () => {
        // 把你的代码写在这里,每次命令执行时都会调用这里的代码
        // ...
        // 给用户显示一个消息提示
        vscode.window.showInformationMessage('Hello World!');
    });
    context.subscriptions.push(disposable);
}

开发插件

测试插件

VS Code 为你的插件提供了运行和调试的能力。测试会运行在一个特殊的 VS Code 实例中——扩展开发环境,这个特殊实例拥有访问 VS Code API 的全部权限。本篇侧重于 VS Code 的集成测试,至于单元测试,你可以使用任何流行的测试框架,如Mocha或者Jasmine

概述


如果你原本使用vscode库进行测试,可以参考从vscode迁移部分

如果你正在使用yo code 生成器,那么生成的项目中应该已经包含了一些测试示例和指引。

使用npm run test或者yarn test启动集成测试,测试工程随后会:

  • 下载并解压最新的 VS Code 版本
  • 运行插件的测试脚本中所规定的Mocha测试

你可以在helloworld-test-sample中找到本篇示例,本篇剩余部分将解释例子中的如下部分:

测试入口


VS Code 提供了 2 个 CLI 参数来运行插件测试——--extensionDevelopmentPath--extensionTestsPath

例如:

# - Launches VS Code Extension Host
# - Loads the extension at <EXTENSION-ROOT-PATH>
# - Executes the test runner script at <TEST-RUNNER-SCRIPT-PATH>
code \
--extensionDevelopmentPath=<EXTENSION-ROOT-PATH> \
--extensionTestsPath=<TEST-RUNNER-SCRIPT-PATH>

测试入口src/test/runTest.ts))使用了vscode-testAPI,简化了下载、解压、启动 VS Code 的测试流程:

import*as path from"path";
import{ runTests }from"vscode-test";
async function main(){
try{
// The folder containing the Extension Manifest package.json
// Passed to `--extensionDevelopmentPath`
const extensionDevelopmentPath = path.resolve(__dirname,"../../");
// The path to the extension test runner script
// Passed to --extensionTestsPath
const extensionTestsPath = path.resolve(__dirname,"./suite/index");
// Download VS Code, unzip it and run the integration test
    await runTests({ extensionDevelopmentPath, extensionTestsPath });
}catch(err){
    console.error("Failed to run tests");
    process.exit(1);
}
}
main();

vscode-test还支持:

  • 启动 VS Code 时打开指定工作区
  • 下载不同版本的 VS Code
  • 使用其他 CLI 参数启动

你可以在microsoft/vscode-test中找到更多用法。

测试脚本


当你运行插件的集成测试时,--extensionTestsPath会指向测试脚本(src/test/suite/index.ts),然后这个脚本会进一步运行测试套件。下面是helloworld-test-sample中的测试脚本,它使用了 Mocha 运行测试套件。你可以把这个文件视为测试的起点,你可以用Mocha 的 API自定义启动时的配置,你也可以用其他任意喜欢的测试框架替代 Mocha。

import*as path from"path";
import*asMochafrom"mocha";
import*as glob from"glob";
exportfunction run():Promise<void>{
// Create the mocha test
const mocha =newMocha({
    ui:"tdd"
});
  mocha.useColors(true);
const testsRoot = path.resolve(__dirname,"..");
returnnewPromise((c, e)=>{
    glob("**/**.test.js",{ cwd: testsRoot },(err, files)=>{
if(err){
return e(err);
}
// Add files to the test suite
      files.forEach(f => mocha.addFile(path.resolve(testsRoot, f)));
try{
// Run the mocha test
        mocha.run(failures =>{
if(failures >0){
            e(newError(`${failures} tests failed.`));
}else{
            c();
}
});
}catch(err){
        e(err);
}
});
});
}

所有测试脚本和*.test.js文件都有访问 VS Code API 的权限。 看看这个例子(src/test/suite/extension.test.ts)

import*asassertfrom"assert";
import{ after }from"mocha";
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import*as vscode from"vscode";
// import * as myExtension from '../extension';
suite("Extension Test Suite",()=>{
  after(()=>{
    vscode.window.showInformationMessage("All tests done!");
});
  test("Sample test",()=>{
assert.equal(-1,[1,2,3].indexOf(5));
assert.equal(-1,[1,2,3].indexOf(0));
});
});

调试测试文件


调试测试文件和调试插件是一样的,我们看一个launch.json调试器配置的例子:

{
"version":"0.2.0",
"configurations":[
{
"name":"Extension Tests",
"type":"extensionHost",
"request":"launch",
"runtimeExecutable":"${execPath}",
"args":[
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles":["${workspaceFolder}/out/test/**/*.js"]
}
]
}

提示


使用 Insider 版本开发插件

由于 VS Code 的限制,如果你使用 VS Code 稳定版来运行集成测试,它会报错:

Running extension tests from the command line is currently only supported ifno other instance of Codeis running.

所以推荐你使用VS Code Insiders测试插件。

调试时禁用其他插件

当你在 VS Code 中对测试进行调试时,VS Code 使用的是全局安装的 VS Code 实例,它会加载所有插件。你可以在launch.json中添加--disable-extensions或者在runTestslaunchArgs选项中添加该项以禁用其他插件。

{
"version":"0.2.0",
"configurations":[
{
"name":"Extension Tests",
"type":"extensionHost",
"request":"launch",
"runtimeExecutable":"${execPath}",
"args":[
"--disable-extensions",
"--extensionDevelopmentPath=${workspaceFolder}",
"--extensionTestsPath=${workspaceFolder}/out/test/suite/index"
],
"outFiles":["${workspaceFolder}/out/test/**/*.js"]
}
]
}

使用vscode-test自定义配置

你可能会需要自定义一些启动配置,比如启动测试前执行code --install-extension安装一些其他插件这样的场景。vscode-test提供粒度更细的 API 来操作这样的场景:

await runTests({
  extensionDevelopmentPath,
  extensionTestsPath,
/**
   * A list of launch arguments passed to VS Code executable, in addition to `--extensionDevelopmentPath`
   * and `--extensionTestsPath` which are provided by `extensionDevelopmentPath` and `extensionTestsPath`
   * options.
   *
   * If the first argument is a path to a file/folder/workspace, the launched VS Code instance
   * will open it.
   *
   * See `code --help` for possible arguments.
   */
  launchArgs:["--disable-extensions"]
});

从 vscode 迁移

vscode中的集成测试模块已迁移到vscode-test,你可以按下面的步骤进行迁移:

  • 移除vscode依赖
  • 添加vscode-test依赖
  • 由于旧的vscode模块会下载 VS Code 类型定义,所以你需要
    • 手动安装@types/vscode,这个版本需和你package.jsonengine.vscode版本一致,比如你的engine.vscode版本是1.30,那么就安装@types/vscode@1.30
    • 删除package.json中的"postinstall": "node ./node_modules/vscode/bin/install"
  • 添加一个测试入口,你可以像我们的示例一样,用runTest.ts作为入口。
  • 指定package.json中的test脚本,运行编译后的runTest.ts
  • 添加一个测试脚本,你可以用sample test runner script作为入口。注意vscode过去依赖mocha@4glob,现在你需要自己安装到devDependency里去。

发布插件

vsce —— 发布工具参阅

vsce是一个用于将插件发布到市场上的命令行工具。

安装


请确认本机已经安装了Node.js,然后运行:

npm install -g vsce

使用


然后你就可以在命令行里直接使用vsce了。下面是一个快速发布的示例(在你登录和打包好插件之后):

$ vsce publish
Publishing uuid@0.0.1...
Successfully published uuid@0.0.1!

更多可用的命令参数,请使用vsce --help

发布教程


!> 注意: 出于安全考虑,vsce不会发布包含用户提供 SVG 图片的插件。

发布工具会检查以下内容:

  • pacakge.json文件中的 icon 不可以是 SVG。
  • pacakge.json中的徽章不可以是 SVG,除非来自于可靠的图标来源
  • README.mdCHANGELOG.md中的图片链接需要使用https协议
  • README.mdCHANGELOG.md中的图片不可以是 SVG,除非来自可靠的图标来源

VS Code 插件市场的服务是Visual Studio Team Services提供的,因此验证、代理、管理插件都是由这个服务提供的。

vsce只能用Personal Access Tokens发布插件,所以至少要创建一个 Token 以便发布插件。

获取 Personal Access Token

首先,你得有一个Azure DevOps 组织

下面的例子里,我们假设组织名为vscode,从你的组织主页(例如:https://dev.azure.com/vscode )进入安全(Security)页面:

选择 Personal Access Token,点击New Token创建一个新的 Personal Access Token

给 Personal Access Token 添加描述,过期时间等等,你最好把过期时间设置为 1 年,这样你接下就能方便很多,选择custom defined(用户自定义)范围,然后点击Show all scopes(显示全部)

最后,在这个列表中找到Marketplace,并勾选AcquireManage

点击Create,你就会看到新创建的 Personal Access Token 了,复制好,你接下来就会用到这个 token 来创建一个发行方了。

创建一个发行方

发行方是 VS Code 市场有权发布插件的唯一标识,每个插件的package.json文件都包含着publisher字段。

现在我们已经有了Personal Access Token,我们马上可以用vsce创建一个发行方:

vsce create-publisher (publisher name)

vsce会记住这个 Personal Access Token,日后你需要再次使用的时候会自动带上。

?> 注意:另外,你也可以在市场的发行方管理页中创建发行方,然后用这个账号登录vsce

发行方登录

如果你已经有发行方账号了:

vsce login (publisher name)

create-publisher命令类似地,vsce会要求输入你的 Personal Access Token。

你也可以用命令参数-p <token>直接登录然后立即发布插件:

vsce publish -p <token>

增量更新插件版本

用 SemVer 语义标识符:majorminorpatch增量更新插件版本号。

例如,你想把插件从 1.0.0 更新到 1.1.0,那么加上minor

vsce publish minor

插件package.json的 version 会先更新,然后才发布插件。

你也可以通过命令行指定版本号:

vsce publish 2.0.1

下架插件

通过指定插件 idpublisher.extension下架插件:

vsce unpublish (publisher name).(extension name)

!> 注意:当你下架插件的时候,市场会移除所有插件的历史统计数据,请在下架前再三考虑,最好还是更新插件吧。

插件打包

你也可能只是想打包一下插件,而不是发布到商店里。用下列命令将插件打包到.vsix文件中:

vsce package

这个命令会在当前目录生成一个.vsix文件,直接从.vsix安装插件是允许的,查看从 VSIX 安装插件了解更多内容。

VS Code 版本兼容性

当你制作插件的时候,你需要描述插件对 VS Code 的版本兼容性——修改package.json中的engines.vscode

{
  "engines": {
    "vscode": "^1.8.0"
  }
}

1.8.0表示你的插件只能兼容1.8.0版本的 VS Code,^1.8.0则表示你的插件向上兼容,包括1.8.1, 1.9.0等等。

使用engines.vscode可以确保插件安装环境包含了插件依赖的 API。这个机制在稳定版和 Insider 版本都适用。

现在我们假设最新的稳定版 API 是1.8.0,而1.9.0引入了新的 API,所以你可以用1.9.0-insider标识插件在 Insider 版中也可正常使用。 如果你想使用这些刚刚引入的 API,则将依赖版本设置为^1.9.0,你的插件则只能安装在>=1.9.0的 VS Code 上,也就意味着所有当前的 Insider 版本都可以用得上,而稳定版只有在更新到1.9.0才能使用你的插件。

进阶用法


符合市场的插件

你可以自定义插件在市场中的外观,查看示例GO 插件

下面是一些让你的插件在市场看起来更酷的小提示:

  • 插件根目录下面的

    README.md

    文件可以用来填充插件市场的页面内容。

    vsce

    会将 README 中的链接通过下面两种方式修改掉:

    • 如果你的package.jsonrepository字段是一个 Github 仓库,vsce会自动检测,然后相应地调整链接。
    • 运行vsce package时,加上--baseContentUrl--baseImagesUrl标识重载上述行为。
  • 插件根目录下的LICENSE会成为插件的 license。

  • 同样的CHANGELOG.md文件会成为插件的发布日志。

  • 通过设置package.jsongalleryBanner.colorhex 值,修改 banner 的背景色。

  • 通过给package.json中的icon设置一个相对路径,可以将一个128px的 PNG 图标放进你的插件中。

  • 参见插件市场展现小提示

.vscodeignore

创建一个.vscodeignore文件可以排除插件目录中的内容。这个文件支持glob模式,每个表达式一行。

例如:

**/*.ts
**/tsconfig.json
!file.ts

你应该忽略哪些不必在运行时用到的文件。例如:你的插件是用 Typescript 写的,那么你就应该忽略所有的**/*.ts文件。

?> 注意:devDependencies列出的开发依赖会被自动忽略,你不必将他们加入到.vscodeignore中。

预发布步骤

你是可以在清单文件中添加预发布步骤的,下面的命令会在插件每次打包时执行:

{
  "name": "uuid",
  "version": "0.0.1",
  "publisher": "joaomoreno",
  "engines": {
    "vscode": "0.10.x"
  },
  "scripts": {
    "vscode:prepublish": "tsc"
  }
}

上面的示例会在每次插件打包时调用 Typescript 编译器。

FAQ

问:当我发布插件时遇到了 403 Forbidden(或 401 Unauthorized)错误?

答:很有可能是你创建 PAT (Personal Access Token) 时没有选择all accessible accounts。你还需要将授权范围设置为All scopes,这样发布工具才能工作。

问:我没办法用vsce工具下架插件?

答:你可能改变了插件 ID 或者发行方名称。不过你还可以在管理页面发布或者下架插件。

问:为什么 vsce 不保留文件属性?

答:请注意,当你在 Windows 平台构建和发布插件的时候,所有插件包内的文件会丢失 POSIX 文件属性,或称之为执行位(executable bit)的东西。一些基于这些属性的node_modules依赖则会调整工作方式。从 Linux 和 macOS 平台构建则会如预期执行。

打包插件

VS Code 插件体积常常随着更新越来越大,它会产生很多文件,而且还依赖各种npm包。 程序开发的最佳实践是抽象和重用,但过度拆分和庞大的代码结构产生的代价就是更大的插件体积和更慢的运行效率。加载 100 个小文件远比加载一个大文件来的慢,这就是我们更推荐打包插件的原因。 打包是将多个小的源文件打包成单个入口文件的过程。

对于 Javascript 而言,可选的构建工具非常多,比较流行的如rollup.jsparcelwebpack。大部分构建工具的概念和功能都是相似的,本节主要使用webpack打包。

使用 webpack


webpack 这个开发工具可以在npm里找到,为了获取 webpack 和它的命令行界面,打开终端然后输入:

npm i --save-dev webpack webpack-cli

这行命令会先安装 webpack,然后更新你插件里的package.json中的devDependencies字段。Webpack 是一个 Javascrip 打包工具,但是大部分 VS Code 插件是用 Typescript 写的,所以你需要在 webpack 中配置ts-loader,它才能正确编译 Typescript。安装ts-loader

npm i --save-dev ts-loader

配置 webpack


既然所有的工具都安装好了,我们现在可以开始配置 webpack 了。通常来说,你的项目目录中需要创建一个webpack.config.js文件,webpack 才能知道按什么规则打包你的插件。下面的配置示例是 VS Code 插件专用的,让我们来开这个头吧:

/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
//@ts-check
"use strict";
const path = require("path");
/**@type {import('webpack').Configuration}*/
const config = {
  target: "node", // vscode插件运行在Node.js环境中 📖 -> https://webpack.js.org/configuration/node/
  entry: "./src/extension.ts", // 插件的入口文件 📖 -> https://webpack.js.org/configuration/entry-context/
  output: {
    // 打包好的文件储存在'dist'文件夹中 (请参考package.json), 📖 -> https://webpack.js.org/configuration/output/
    path: path.resolve(__dirname, "dist"),
    filename: "extension.js",
    libraryTarget: "commonjs2",
    devtoolModuleFilenameTemplate: "../[resource-path]"
  },
  devtool: "source-map",
  externals: {
    vscode: "commonjs vscode" // vscode-module是热更新的临时目录,所以要排除掉。 在这里添加其他不应该被webpack打包的文件, 📖 -> https://webpack.js.org/configuration/externals/
  },
  resolve: {
    // 支持读取TypeScript和JavaScript文件, 📖 -> https://github.com/TypeStrong/ts-loader
    extensions: [".ts", ".js"]
  },
  module: {
    rules: [
      {
        test: /\.ts$/,
        exclude: /node_modules/,
        use: [
          {
            loader: "ts-loader"
          }
        ]
      }
    ]
  }
};
module.exports = config;

这份文件是webpack-extension中的一部分。webpack 配置最后输出的就是 JS 对象。

在上面的例子里,我们定义了如下内容:

  • traget:’node’,因为我们的插件运行在 Node.js 环境中。
  • entry:webpack 使用的入口文件。这就像是package.json中的main属性,有点不一样的是你还需要给 webpack 提供”source”—— 一般就是src/extension.ts,小心不要配置在”output”里了。webpack 可以解析 Typescript,所以我们不需要再单独执行 Typescript 编译了。
  • output配置告诉 webpack 应该把打包好的文件放在哪里,一般而言我们会放在dist文件夹里。在这个例子里,webpack 最后会产生一个dist/extension.js文件。
  • resolvemodule/rules中配置 Typescript 和 Javascript 的解析器。
  • externals即排除配置,在这里可以配置打包文件不应包含的文件和模块。vscode不需要被打包是因为它并不储存在磁盘上,它是 VS Code 热更新生成的临时文件夹。根据插件依赖的具体 node 模块,你可能需要通过这个配置优化打包文件。

运行 webpack


webpack.config.js文件创建好之后,webpack 就可以正式开始工作了。你可以从命令行中运行 webpack,不过为了避免重复工作用 npm script 会更有效率。

将下列脚本复制到package.jsonscripts中去:

"scripts": {
  "vscode:prepublish": "webpack --mode production",
  "compile": "webpack --mode none",
  "watch": "webpack --mode none --watch",
},

compilewatch脚本是开发时使用的,它们会产生构建文件。vscode:prepublishvsce使用的,vsce是 VS Code 的打包和发布工具,你需要在发布插件之前运行这个命令。webpack 中的mode是控制优化级别的配置项,如果你使用production字段,那么就会打包出最小的构建文件,但是也会耗费更多时间,所以我们开发中使用none。想要运行上述脚本,我们可以打开终端(命令行)输入npm run compile或者从命令面板(Ctrl+Shift+P)中使用运行任务来开始。

运行插件


运行插件之前,你需要将package.json中的main属性指向到构建文件上,也就是我们上面提到的"./dist/extension",改好之后我们就可以运行和测试插件了。关于调试配置,请注意更新launch.json中的outFiles属性。

测试


插件开发者一般都会给插件源代码进行单元测试,但是有了完备的底层架构支持,插件的源代码可以不依赖测试,webpack 产生的构建文件中也不应该包含任何测试代码。如果需要运行单元测试,只需要简单地编译就好了。在上面的例子里,我们有一个test-compile脚本,它会把调用 Typescript 编译器将插件编译至out目录中。这样一来我们就有了 JS 文件,再使用下面的launch.json就足够应付测试了。

{
  "name": "Extension Tests",
  "type": "extensionHost",
  "request": "launch",
  "runtimeExecutable": "${execPath}",
  "args": [
    "--extensionDevelopmentPath=${workspaceFolder}",
    "--extensionTestsPath=${workspaceFolder}/out/test"
  ],
  "outFiles": ["${workspaceFolder}/out/test/**/*.js"],
  "preLaunchTask": "npm: test-compile"
}

这个测试配置对于非 webpack 打包的插件来说也是可以使用的。我们没必要将单元测试打包起来,因为它们不应包含在我们发布的插件里。

发布


发布前你需要更新.vscodeignore文件。现在所有东西都打包到了dist/extension.js文件中,所以应该排除这个文件还有out文件夹(怕你漏了,特此提醒),以及最重要的node_modules文件夹。

一般来说,.vsignore文件应该是这样的:

.vscode
node_modules
out/
src/
tsconfig.json
webpack.config.js

迁移插件


用 webpack 迁移现有的插件是很容易的,整个过程就像我们上面的指南一样。真实的例子如 VS Code 的 References 视图就是从这个pull request应用了 webpack 而来的。

在里面,你可以看到:

  • devDependencies中添加webpackwebpack-clits-loader
  • 更新 npm 脚本以便开发时使用 webpack
  • 更新调试配置文件launch.json
  • 添加和修改webpack.config.js
  • 更新.vscodeignore排除node_modules和其他开发时产生的临时文件
  • 开始享受体积更小、安装更快的插件!

疑难解答


压缩

使用production模式会执行代码压缩,它会去除源代码中的空格和注释,并把变量名和函数名进行替换——混淆和压缩。不过形如Function.prototype.name的代码不会压缩。

webpack critical dependencies

当你运行 webpack 时,你可能会碰到像Critical dependencies: the request of a dependency is an expression字样的警告。这些警告必须立即处理,一般来说会影响到打包过程。这句话意味着 webpack 不能静态分析某些依赖,一般是由动态使用require导致的,比如require(someDynamicVariable)

想要处理这类警告,你需要:

  • 将需要打包的部分变成静态的依赖。
  • 通过externals排除这部分依赖,但是注意它们的 Javascript 文件还是应该保留在我们打包的插件里,在.vscodeignore中使用 glob 模式,比如!node_modules/mySpecialModule

持续集成

插件测试也可以用诸如Travis CI自动运行测试。vscode-test库可以基于 CI 设置插件测试,而且里面还包含了一个 Azure Pipelines 的示例插件。你可以先看看构建管线是什么样子的,或者直接查看azure-pipelines.ymlfile

Azure Pipelines


你可以在Azure DevOps上创建免费的项目,它为你提供了代码托管、看板、构建和测试基础设施等等。最重要的是,你可以获得10 个免费的并行任务容量,用于你构建项目,不论是在 Windows, macOS 还是 Linux 上。

首先,你需要创建一个免费的Azure DevOps账号,然后给你的插件创建一个Azure DevOps 项目

然后,把azure-pipelines.yml文件添加到插件仓库的根目录下,不同于 Linux 中的xvfb配置脚本,需要 VS Code 运行在 Linux 的无头 CI 机器上,我们的配置文件非常简单:

trigger:
  - master
strategy:
  matrix:
    linux:
      imageName: "ubuntu-16.04"
    mac:
      imageName: "macos-10.13"
    windows:
      imageName: "vs2017-win2016"
pool:
  vmImage: $(imageName)
steps:
  - task: NodeTool@0
    inputs:
      versionSpec: "8.x"
    displayName: "Install Node.js"
  - bash: |
      /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
      echo ">>> Started xvfb"
    displayName: Start xvfb
    condition: and(succeeded(), eq(variables['Agent.OS'], 'Linux'))
  - bash: |
      echo ">>> Compile vscode-test"
      yarn && yarn compile
      echo ">>> Compiled vscode-test"
      cd sample
      echo ">>> Run sample integration test"
      yarn && yarn compile && yarn test
    displayName: Run Tests
    env:
      DISPLAY: ":99.0"

最后,在你的 DveOps 项目里创建一个新的管线,然后指向azure-pipelines.yml文件,启动 build,然后……真香~

你可以启用持续构建——每当有 pull requests 进入特定分支的时候自动进行构建。相关内容请查看构建管线触发器

Travis CI


vscode-test还包含了一份Travis CI 构建文件,因为 Travis 上的环境变量定义和 Azure 所有不同,xvfb脚本也有些许不一样:

language: node_js
os:
  - osx
  - linux
node_js: 8
install:
  - |
    if [ $TRAVIS_OS_NAME == "linux" ]; then
      export DISPLAY=':99.0'
      /usr/bin/Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 &
    fi
script:
  - |
    echo ">>> Compile vscode-test"
    yarn && yarn compile
    echo ">>> Compiled vscode-test"
    cd sample
    echo ">>> Run sample integration test"
    yarn && yarn compile && yarn test
cache: yarn

插件功能

概述

VS Code 提供了非常多的方法,供插件扩展VS Code本身的能力。但是有的时候也很难找到对的发布内容配置和VS Code API。这章内容将插件的功能分成了几个部分,每个部分都将告诉你:

  • 插件可以使用的功能
  • 这些功能点的细节索引
  • 一些插件灵感

不过,我们也会告诉你一些限制,为了避免插件影响到VS Code的性能和稳定性。比如:插件不可以修改VS Code UI底层的DOM。

常用功能


常用功能是你在任何插件中都可能用到的核心功能。

这些功能包括:

  • 注册命令、配置、快捷键绑定、菜单等。
  • 保存工作区或全局数据。
  • 显示通知信息。
  • 使用快速选择获得用户输入。
  • 打开系统的文件选择工具,以便用户选择文件或文件夹。
  • 使用进度API提示耗时较长的操作。

主题


主题控制着VS Code的外观——编辑器中的源代码的颜色和VS Code UI颜色。如果你曾经想要把VS Code搞成绿色,想象自己在黑客帝国里写代码,或者想要追求极简性冷淡灰色风格,那么主题章节就是为你而来。

插件灵感
  • 改变你的代码颜色
  • 改变VS Code UI颜色
  • 将现有的TextMate主题应用到VS Code中
  • 添加自定义图标

声明式添加语言特性


声明式语言特性添加了基础的编程语言编辑支持,如括号匹配、自动缩进和语法高亮。这些功能都可以通过声明配置而不用写任何代码就可以获得,更高级的语言特性如IntelliSense或调试,请看编程式添加语言特性

插件灵感
  • 将常用的JS代码片段打包到插件中
  • 为VS Code添加新的语言支持
  • 为一门语言添加或替换语法
  • 通过注入的方式,扩展一门语法
  • 将现有的 TextMate 语法迁移到VS Code中

编程式添加语言特性


编程式添加语言特性可以为编程语言添加更为丰富的特性,如:悬停提示、转跳定义、错误诊断、IntelliSense和CodeLens。这些语言特性暴露于vscode.languages.*API。语言插件可以直接使用这些API,或是自己写一个语言服务器,通过语言服务器库将它适配到VS Code。

虽然我们提供了一个语言特性列表,但是并不阻碍你发挥想象,自由使用这些API。比方说,在行内显示额外信息,使用CodeLens和代码悬停是非常好的方式,而错误诊断可以高亮拼写或代码风格错误。

插件灵感
  • 鼠标悬停于API上时, 出现用法示例
  • 使用诊断,报告代码风格错误
  • 注册新的HTML代码格式化
  • 提供丰富的IntelliSense中间件
  • 为一门语言添加代码折叠、面包屑、轮廓支持

扩展工作台


扩展工作台加强了 VS Code 工作台的UI,为资源管理侧边栏添加了新的右击行为,你甚至可以用 TreeView API构建自定义的资源管理侧边栏。如果你的插件需要完全自定义用户界面,那就使用Webview API和HTML,CSS,Javascript构建你自己的UI。

插件灵感
  • 自定义资源管理侧边栏的菜单行为
  • 在侧边栏中创建新的、交互式的TreeView
  • 定义新的活动栏视图
  • 在状态栏显示新的信息
  • 使用WebView API显示自定义内容
  • 配置源控制(git/svn等)来源

调试


你可以利用调试来制作调试器插件,这个插件需要将VS Code的调试UI连接到真实的调试器或者运行时上。

插件灵感
  • 通过调试适配器将VS Code的调试UI连接到真实的调试器或者运行时上
  • 通过调试器插件添加语言支持
  • 为调试配置文件添加丰富的智能提示或者悬停信息
  • 为调试配置文件添加代码片段

另一方面,VS Code也提供了非常多的调试器插件API,你可以用来实现任何VS Code调试器相关的功能,加强用户的调试体验。

插件灵感
  • 动态生成调试器配置文件,启动调试器会话
  • 跟踪调试会话的声明周期
  • 编程式管理断点

限制


最后,我还对插件也提出了一些限制。

不可访问DOM

插件没有权限访问VS Code UI的底层DOM,禁止添加自定义的CSS 和 HTML片段到VS Code UI上。

我们在一直努力优化VS Code底层的web技术,为用户创造高可用、持续响应的编辑器,而我们也会继续调整这些技术中使用到的DOM。为了确保不会干扰到VS Code的性能和稳定性,同时不阻断其他插件的运行,所以我们的插件都跑在插件主机进程中,而且阻止了插件直接访问DOM的途径。

常用功能

常用功能对你的插件来说非常重要,几乎所有的都会或多或少地用到这些功能,下面我们为你简单地介绍一下它们。

命令


命令是VS Code 运作的核心。你可以打开命令面板执行,用快捷键执行,还可以在菜单中鼠标右键执行。

一个插件应该:

  • 使用vscode.commands注册和执行命令
  • 配置contributes.commands,确保命令面板中可以顺利执行你注册的命令

配置


插件需要在contributes.configuration发布内容配置点中填写有关的配置,你可以workspace.getConfiguration API中阅读有关内容。

键位绑定


插件可以添加自定义键位映射,在contributes.keybindings键位绑定中了解更多有关内容。

菜单


插件可以自定义上下文菜单项,菜单会根据用户右击VS Code UI的不同位置而各不相同。查看更多contributes.menus发布内容配置。

数据储存


VS Code中有三种数据储存方式:

插件的执行上下文在activate函数。

显示通知


几乎所有的插件都需要在某些时候为用户提示信息。VS Code提供了3个API来展示不同重要程度的信息:

快速选择


使用vscode.QuickPickAPI,你可以轻松地收集用户输入或者为用户显示选择列表。快速输入 示例将详细解释这个API。

文件选择


插件可以使用vscode.window.showOpenDialogAPI打开系统文件选择器,然后选择文件或是文件夹。

输出渠道


输出面板显示了一组输出渠道,以便于你查看日志。你可以使用window.createOutputChannel创建一个新的输出渠道。

进度API


使用vscode.Progress将处理进度报告给用户。

通过ProgressLocation选项,进度可以显示在不同的区域:

  • 显示在通知区
  • 显示在源控制视图
  • VS Code窗口中的通用进度条位置

详见进度 示例

主题

VS Code中的主题分为两类:

  • 色彩主题:UI组件ID和文本符号ID到色彩间的映射。通过色彩主题你可以修改VS Code UI组和编辑器中的文本。
  • 图标主题:文件类型/名称到图片之间的映射。文件图标显示于VS Code的资源管理侧边栏、快速打开列表和编辑器Tab等UI中。

色彩主题


如上图所示,色彩主题定义了2种映射:

  • colors控制UI组件的颜色
  • tokenColors控制了每个代码标记单元(你的代码会根据语法分割成一个个标记单元)的颜色。

创建主题详见色彩主题指南和色彩主题 示例

图标主题


图标主题允许你:

  • 将图标ID映射至图片或者字体图标上。
  • 根据文件名或这个文件的语言类型关联上图标ID

扩展工作台

“工作台”是指整个VS Code UI和其中包含的下列UI 组件:

  • 标题栏
  • 活动栏
  • 侧边栏
  • 面板
  • 编辑器群
  • 状态栏

VS Code提供了各式各样的API让在工作台你添加自己的组件。比如下图:

视图容器


contributes.viewsContainers发布内容配置中,你可以添加新的视图容器在5个内置的视图容器中。学习更多树视图。

树视图


contributes.views发布内容配置中,你可以添加在任何视图容器岁添加新的视图。学习更多树视图。

Webview


Webview是使用HTML/CSS/JS高度定制的视图。它们显示在编辑器区域中。详见Webview指南。

状态栏项


插件可以创建自定义的StatusBarItem显示在状态栏中。状态栏项可以显示文本和图标,还可以在点击事件触发时运行命令。

  • 显示文本和图标
  • 点击时运行命令

状态栏插件的例子可以戳这里

插件指南

概述

通过Hello World学习过基本的VS Code插件API之后,现在是时间搞搞真正的插件了。在插件功能章节,在更宽泛的层面上介绍了插件能做些什么,本章则细化了各个功能点,并提供了详细的代码和VS Code API的解释。

在每个指南-示例组合中,你将会看到:

  • 贯穿整个代码的注释
  • 使用示例插件的gif或者图片
  • 运行示例插件指引
  • 使用过的VS Code API列表
  • 使用过的配置点列表
  • 真正的组合插件示例
  • API概念解释

指南和例子


下面是一份指南和例子的表格,虽然每个指南都要有示例代码,但是一些示例目前暂时还没有与之匹配的指南。

每个例子都至少阐释了一个VS Code API或者发布内容配置的使用。

例子 VS Code网页指南 API & 配置
Webview Sample /extension-guides/webview window.createWebviewPanel window.registerWebviewPanelSerializer
Status Bar Sample N/A window.createStatusBarItem StatusBarItem
Tree View Sample /extension-guides/tree-view window.createTreeView window.registerTreeDataProvider TreeView TreeDataProvider contributes.views contributes.viewsContainers
Task Provider Sample /extension-guides/task-provider tasks.registerTaskProvider Task ShellExecution contributes.taskDefinitions
Multi Root Sample N/A workspace.getWorkspaceFolder workspace.onDidChangeWorkspaceFolders
Completion Provider Sample N/A languages.registerCompletionItemProvider CompletionItem SnippetString
File System Provider Sample N/A workspace.registerFileSystemProvider
Editor Decorator Sample N/A TextEditor.setDecorations DecorationOptions DecorationInstanceRenderOptions ThemableDecorationInstanceRenderOptions window.createTextEditorDecorationType TextEditorDecorationType contributes.colors
I18n Sample N/A
Terminal Sample N/A window.createTerminal window.onDidChangeActiveTerminal window.onDidCloseTerminal window.onDidOpenTerminal window.Terminal window.terminals
Vim Sample N/A commands StatusBarItem window.createStatusBarItem TextEditorCursorStyle window.activeTextEditor Position Range Selection TextEditor TextEditorRevealType TextDocument

语言插件示例


下面的部分是语言插件相关示例:

例子 VS Code网页指南
Snippet Sample /language-extensions/snippet-guide contributes.snippets
Language Configuration Sample /language-extensions/language-configuration-guide contributes.languages
LSP Sample /language-extensions/language-server-extension-guide
LSP Log Streaming Sample N/A
LSP Multi Root Server Sample https://github.com/Microsoft/vscode/wiki/Adopting-Multi-Root-Workspace-APIs#language-client--language-server

命令

命令会触发VS Code中注册的行为,如果你配置过键位,那么你就处理过了命令。命令也是插件将功能暴露给用户的地方,它绑定了VS Code UI中的行为,并在内部处理了相关逻辑。

使用命令


VS Code内部含有大量和编辑器交互、控制UI、后台操作的内置命令。许多插件将它们的核心功能暴露为命令的形式供用户或者其他插件使用。

程序性执行一个命令

vscode.commands.executeCommandAPI可以程序性调用一个命令,你可以通过它将VS Code的内置函数构建在你的插件中,比如VS Code内置的Git和Markdown插件中的东西。

我们看个例子🌰:editor.action.addCommentLine命令可以将当前选中行变成注释(你可以偷偷把这个功能地集成到你自己的插件中哦):

import * as vscode from 'vscode';
function commentLine() {
    vscode.commands.executeCommand('editor.action.addCommentLine');
}

有些命令可以接收改变行为的参数,有些会有返回结果。形如vscode.executeDefinitionProvider的API,它要求传入一个document的URI地址和position作为参数,并返回一个包含定义列表的promise:

import * as vscode from 'vscode';
async function printDefinitionsForActiveEditor() {
    const activeEditor = vscode.window.activeTextEditor;
    if (!activeEditor) {
        return;
    }
    const definitions = await vscode.commands.executeCommand<vscode.Location[]>(
        'vscode.executeDefinitionProvider',
        activeEditor.document.uri,
        activeEditor.selection.active
    );
    for (const definition of definitions) {
        console.log(definition);
    }
}

更多命令详见:

命令的URLs

命令URI是执行注册命令的链接。它们可被用于悬停文本上的可点击链接,代码补全提示中的细节信息,甚至可以出现在webview中。

命令URI使用command作为协议头,然后紧跟着命令名称。比如:editor.action.addCommentLine的命令URI是:command:editor.action.addCommentLine。下面是一个显示在当前行注释中显示链接的悬停函数。

import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
    vscode.languages.registerHoverProvider(
        'javascript',
        new class implements vscode.HoverProvider {
            provideHover(
                _document: vscode.TextDocument,
                _position: vscode.Position,
                _token: vscode.CancellationToken
            ): vscode.ProviderResult<vscode.Hover> {
                const commentCommandUri = vscode.Uri.parse(`command:editor.action.addCommentLine`);
                const contents = new vscode.MarkdownString(`[Add comment](${commentCommandUri})`);
                // command URIs如果想在Markdown 内容中生效, 你必须设置`isTrusted`。
                // 当创建可信的Markdown 字符, 请合理地清理所有的输入内容
                // 以便你期望的命令command URIs生效
                contents.isTrusted = true;
                return new vscode.Hover(contents);
            }
        }()
    );
}

命令上的参数列表会从JSON数组变成URI格式:下面的例子使用了git.stage命令创建一个悬停操作——将当前文件进行git暂存:

import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
    vscode.languages.registerHoverProvider(
        'javascript',
        new class implements vscode.HoverProvider {
            provideHover(
                document: vscode.TextDocument,
                _position: vscode.Position,
                _token: vscode.CancellationToken
            ): vscode.ProviderResult<vscode.Hover> {
                const args = [{ resourceUri: document.uri }];
                const commentCommandUri = vscode.Uri.parse(
                    `command:git.stage?${encodeURIComponent(JSON.stringify(args))}`
                );
                const contents = new vscode.MarkdownString(`[Stage file](${commentCommandUri})`);
                contents.isTrusted = true;
                return new vscode.Hover(contents);
            }
        }()
    );
}

新建命令


注册一个命令

vscode.commands.registerCommand会把命令ID绑定到你插件的函数上:

import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
    const command = 'myExtension.sayHello';
    const commandHandler = (name?: string = 'world') => {
        console.log(`Hello ${name}!!!`);
    };
    context.subscriptions.push(vscode.commands.registerCommand(command, commandHandler));
}

只要myExtension.sayHello命令执行,就会调用对应的处理函数,你也可以通过executeCommand程序性调用它,或者从VS Code UI中,抑或快捷键的方式调用。

创建面向用户的命令

vscode.commands.registerCommand仅仅是将命令id绑定到了处理函数上,如果想让用户从命令面板中找到你的命令,你还需要在package.json中配置对应的命令配置项(contribution)

{
    "contributes": {
        "commands": [
            {
                "command": "myExtension.sayHello",
                "title": "Say Hello"
            }
        ]
    }
}

commands配置告诉VS Code你的插件提供了一个命令,而且允许你控制命令在UI中的显示。现在,我们的命令终于出现在命令面板中了:

我们依旧需要使用registerCommand将真实的命令id绑定到函数上。也就是说,如果我们的插件没有激活,那么用户从命令面板中选择myExtension.sayHello也不会有任何效果。为了避免这种事,插件必须注册一个面向全部用户场景的命令onCommand activiationEvent

{
    "activationEvents": ["onCommand:myExtension.sayHello"]
}

现在当用户第一次调用myExtension.sayHello时,插件就会自动激活,registerCommand会将myExtension.sayHello绑定到正确的处理函数上。

对于内部命令你不需要使用onCommand,但是下面的场景中你必须定义好激活事件:

  • 需要使用命令面板调用
  • 需要快捷键调用
  • 需要通过VS Code UI调用,比如在编辑器标题栏上触发
  • 意在供其他插件使用时

控制命令出现在命令面板的时机

默认情况下,所有命令面板中出现的命令都可以在package.jsoncommands部分中配置。不过,有些命令是场景相关的,比如在特定的语言的编辑器中,或者只有用户设置了某些选项时才展示。

menus.commandPalette发布内容配置运行你限制命令出现在命令面板的时机。你需要配置命令ID和一条when语句

{
    "contributes": {
        "menus": {
            "commandPalette": [
                {
                    "command": "myExtension.sayHello",
                    "when": "editorLangId == markdown"
                }
            ]
        }
    }
}

现在myExtension.sayHello命令只会出现在用户的Markdown文件中了。

色彩主题

色彩可视化工作在VS Code可以分成两种类型:

  • 工作台(Workbench)色彩:在视图和编辑器中使用的工作台(Workbench)色彩,包括视图、编辑器、活动栏、状态栏等。
  • 语法色彩:语法色彩就是编辑器中代码的颜色,它基于TextMate语法和TextMate主题规则进行着色。

下面将分别介绍这两种类型。

工作台色彩


创建工作台色彩最简单的方式就是使用现成的主题,然后开始定制。

  • 在VS Code中切换到你想要编辑的色彩主题。
  • 打开设置,用workbench.colorCustomizations修改视图和编辑器色彩。一般来说会即时生效,如果没有生效你需要自己重启VS Code。

下面是一个改变了标题栏颜色的配置

{
    "workbench.colorCustomizations": {
        "titleBar.activeBackground": "#ff0000"
    }
}

语法色彩


新建语法高亮色彩有两种方式:

  • 直接使用社区现成的TextMate主题(.tmTheme文件)
  • 自己想一个主题规则出来

当然,还有更简单的方式:

  • 切换到色彩主题,用设置中的editor.tokenColorCustomizations进行自定义,就像上面修改工作台色彩一样,修改会立即呈现,你不需要重启VS Code。

下面的例子修改了编辑器中注释的颜色:

{
    "editor.tokenColorCustomizations": {
        "comments": "#FF0000"
    }
}

设置支持一些简单的语法标识模型,比如“comments”,“strings”,“numbers”等等。如果你想定制更多颜色,那么直接应用TextMate语法规则就可以了。

创建新的色彩主题


既然你已经用过workbench.colorCustomizationseditor.tokenColorCustomizations笨拙地修改过颜色,那么接下来我们可以见识见识大杀器了。

  • 打开命令面板输入Developer: Generate Color Theme from Current Settings

  • 使用VS Code的Yeoman插件生成器,生成新的主题:

    npm install -g yo generator-code
    yo code
  • 如果你像下图这样选择了自定义主题,则选择’Start fresh’

  • 把从设置中生成的主题文件复制到新的插件中
  • 如果你想使用现成的TextMate主题,那你就需要在插件生成的时候选择导入TextMate主题并打包。另外,如果你下载了一个主题,那么只要用.tmTheme链接替换tokenColors部分就可以了。
{
    "type": "dark",
    "colors": {
        "editor.background": "#1e1e1e",
        "editor.foreground": "#d4d4d4",
        "editorIndentGuide.background": "#404040",
        "editorRuler.foreground": "#333333",
        "activityBarBadge.background": "#007acc",
        "sideBarTitle.foreground": "#bbbbbb"
    },
    "tokenColors": "./Diner.tmTheme"
}

?> 为你的色彩文件添加.color-theme.json前缀,那么你在编辑这个文件时就能自动获得悬浮提示、代码补全、色彩装饰器和色彩选择器等功能了。

?> ColorSublime有成百上千个现成的TextMate主题。你可以选择一个你喜欢的主题,复制下载链接,然后用Yeoman选择这个主题生成你的插件。格式如:"https://raw.githubusercontent.com/Colorsublime/Colorsublime-Themes/master/themes/(name).tmTheme"

测试新的主题


通过按F5打开一个插件主机开发窗口,来测试主题。

通过文件 > 首选项 > 颜色主题,在下拉菜单里找到你的主题。然后通过移动上下箭头,预览你自己的主题。

主题文件的改动,会实时同步到插件开发主机窗口。

将主题发布到插件市场


如果你想把主题分享给社区,通过插件市场去发布它吧。用vsce publishing tool打包你的主题然后发布到VS Code市场。

?> 小贴士:想要用户轻松地找到你的主题,最好修改一下package.json,把关键字”theme”写到插件描述(extension description)中,然后把Category设置为Theme

添加新的色彩id


色彩配置点可以配置插件的色彩id,当在workbench.colorCustomizations和主题文件中使用代码补全时,这些色彩也会出现。用户可以在插件配置选项卡中看到插件定义了什么颜色。

更多

图标主题

VS Code的UI在文件名称左边显示图标,插件配置的图标系列可以让用户自由选择他们喜爱的图标。

添加新的图标主题


你能使用图标文件(最好是SVG)和字体图标创建自己的图标主题。作为示例,你可以参考一下2个内置主题:MinimalSeti

首先,创建一个VS Code插件,然后把iconTheme配置点(contribution point)添加进去

"contributes": {
    "iconThemes": [
        {
            "id": "turtles",
            "label": "Turtles",
            "path": "./fileicons/turtles-icon-theme.json"
        }
    ]
}
  • id作为这个图标主题的标识,目前只做内部使用,未来可能会用在设置里面,所以最好设置一个可读性强的唯一值。
  • label会显示在主题选择下拉框中。
  • path指示了图标集所在的位置。如果你的图标系列名称遵循*icon-theme.json命名规范,那么VS Code就能提供完整的支持。

图标文件集(Icon set file)

图标文件集是一个JSON文件,包含了所有的关联图标和图标定义。

一个关联图标图标定义映射到一个文件上(类型如:文件,文件夹,json文件…)。图标定义指示了图标的所在位置:可以是一个图片文件,或者glyph字体。

图标定义

iconDefinitions部分包含了所有定义。每个定义有一个id,用于指向定义。一个定义能供多个文件关联引用。

"iconDefinitions": {
    "_folder_dark": {
        "iconPath": "./images/Folder_16x_inverse.svg"
    }
}

这里,图标定义包含了一个标识符_folder_dark。除此之外还支持以下属性:

  • iconPath:当使用svg/png文件时:指向图片的路径。
  • fontCharacter:当使用glyph字体时:字体中使用的字符。
  • fontColor:当使用glyph字体时:设置glyph的颜色。
  • fontSize:当使用字体时:设置字体大小。默认情况下会使用字体本身定义的字体大小。这个值应为父级字号的相对值(如 150%)。
  • fontId:当使用字体时: 字体的ID。如果没有指定,则会采用font specification部分的第一个字体。

关联文件

图标能关联到文件夹,文件夹名称,文件,文件名称,文件插件,和语言Id。

这些关联都能被提炼为诸如’light’和’highContrast’色彩主题。

每个文件关联指向一个图标定义

"file": "_file_dark",
"folder": "_folder_dark",
"folderExpanded": "_folder_open_dark",
"folderNames": {
    ".vscode": "_vscode_folder",
},
"fileExtensions": {
    "ini": "_ini_file",
},
"fileNames": {
    "win.ini": "_win_ini_file",
},
"languageIds": {
    "ini": "_ini_file"
},
"light": {
    "folderExpanded": "_folder_open_light",
    "folder": "_folder_light",
    "file": "_file_light",
    "fileExtensions": {
        "ini": "_ini_file_light",
    }
},
"highContrast": {
}
  • file是一个默认文件图标,为那些没有匹配到任何插件、文件名、语言类型的文件所准备的。目前所有文件图标属性都会被继承(只适用于:glyphs字体、字体大小(fontSize))。
  • folder收起的文件夹图标,如果folderExpanded没有设置,那么展开的文件夹也会使用这个图标。使用folderNames关联特殊名称的文件夹。文件夹图标是可选的,如果不设置,那文件夹就不会显示任何图标。
  • folderExpanded展开的文件夹图标。这个图标是可选的,如果不设置就会使用folder定义好的图标。
  • folderNames特殊名称文件夹图标。这个键是用于文件夹名称的,不支持包含路径的名称,不支持匹配模式和通配符。大小写不敏感。
  • folderNamesExpanded展开的特殊名称文件夹图标。
  • rootFolder 收起的工作区根文件夹图标,如果rootFolderExpanded没有设置,那么展开的工作区根文件夹也会使用这个图标。如果不设置,则会使用folder定义的文件夹图标。
  • rootFolderExpanded 展开的工作区根文件夹图标。如果没有设置,则会使用rootFolder定义的文件夹图标。
  • languageIds语言类型图标。这个键将匹配在语言配置点(contribution point)配置的语言id。注意语言配置的’第一行’是不考虑在内的。
  • fileExtensions文件插件图标。根据文件插件的名称匹配。插件名称是文件名点号后面(不包含点号)。拥有多重点号的文件名称,如lib.d.ts会匹配多个模式——d.tsts。大小写敏感。
  • fileNames文件图标。这个键需要文件的全称进行匹配,不支持包含路径的名称,不支持模式和通配符。大小写敏感。fileNames是最高优先匹配。

匹配优先级:fileNames > fileExtensions > languageIds

lighthighContrast部分的属性表和上面相同,只是会在对应的主题下覆盖原有图标配置。

字体定义

在’font’部分添加任意你喜欢的字形和字体。定义好之后,你就可以在图标定义中使用它们了。如果没有指定字体id,那么默认使用第一个定义的字体。

将字体文件移动到你的插件中,设置好对应的路径。推荐使用WOFF字体。

  • 设置格式为’woff’
  • 字重键值的定义参考这里
  • 样式键值对的定义参考在这里
  • 使用图标引用该字体时的字号。因此字体字号总是以百分比表示。
"fonts": [
    {
        "id": "turtles-font",
        "src": [
            {
                "path": "./turtles.woff",
                "format": "woff"
            }
        ],
        "weight": "normal",
        "style": "normal",
        "size": "150%"
    }
],
"iconDefinitions": {
    "_file": {
        "fontCharacter": "\\E002",
        "fontColor": "#5f8b3b",
        "fontId": "turtles-font"
    }
}

图标主题中的文件夹图标

文件图标主题会告诉文件浏览器不要显示默认文件夹图标(倒三角或者横杠),这个模式可在配置中加入"hidesExplorerArrows":true覆盖默认VS Code的设置。

树视图

本节将教你如何为VS Code添加视图容器树视图的插件,示例插件的源代码请查看https://github.com/Microsoft/vscode-extension-samples/tree/master/tree-view-sample。

视图容器


视图容器包含了一列视图(views),这些视图又包含在内置的视图容器中。

要想配置一个视图容器,你首先得注册package.json中的contributes.viewsContainers。你还必须配置以下字段:

  • id: 新视图容器的名称
  • title: 展示给用户的视图容器名称,它会显示在视图容器上方
  • icon: 在活动栏中展示的图标
"contributes": {
  "viewsContainers": {
    "activitybar": [
      {
        "id": "package-explorer",
        "title": "Package Explorer",
        "icon": "media/dep.svg"
      }
    ]
  }
}

树视图


视图是显示在视图容器中的UI片段。使用contributes.views进行配置,你就可以将新的视图添加到内置或者你配置好的视图容器中了。

要想配置一个视图,你首先得注册package.json中的contributes.views。你必须给视图配置一个ID外加一个名称,你还可以配置视图出现的位置:

  • explorer: 显示在资源管理器侧边栏
  • debug: 显示在调试侧边栏
  • scm: 显示在源代码侧边栏
  • test: 测试侧边栏中的资源管理器视图
  • 显示在你定义好的视图容器
"contributes": {
  "views": {
    "package-explorer": [
      {
        "id": "nodeDependencies",
        "name": "Node Dependencies",
        "when": "explorer"
      }
    ]
  }
}

当用户打开了对应的视图,VS Code会触发onView:${viewId}事件(如上面例子中,这个事件写为onView:nodeDependencies)。你也可以通过配置when字段控制视图的展示。

视图的操作


你可以配置视图下述位置的操作:

  • view/title: 视图标题位置显示的操作。这里可以配置主要的操作,使用"group": "navigation"进行配置,剩余的二级操作则出现在...菜单中。
  • view/item/context: 每个视图项的操作。这里可以配置主要的操作,使用"group": "inline",剩余的二级操作则出现在...菜单中。

使用when属性控制这些操作的展示。

例如:

"contributes": {
  "commands": [
    {
      "command": "nodeDependencies.refreshEntry",
      "title": "Refresh",
      "icon": {
        "light": "resources/light/refresh.svg",
        "dark": "resources/dark/refresh.svg"
      }
    },
    {
      "command": "nodeDependencies.addEntry",
      "title": "Add"
    },
    {
      "command": "nodeDependencies.editEntry",
      "title": "Edit",
      "icon": {
        "light": "resources/light/edit.svg",
        "dark": "resources/dark/edit.svg"
      }
    },
    {
      "command": "nodeDependencies.deleteEntry",
      "title": "Delete"
    }
  ],
  "menus": {
    "view/title": [
      {
        "command": "nodeDependencies.refreshEntry",
        "when": "view == nodeDependencies",
        "group": "navigation"
      },
      {
        "command": "nodeDependencies.addEntry",
        "when": "view == nodeDependencies"
      }
    ],
    "view/item/context": [
      {
        "command": "nodeDependencies.editEntry",
        "when": "view == nodeDependencies && viewItem == dependency",
        "group": "inline"
      },
      {
        "command": "nodeDependencies.deleteEntry",
        "when": "view == nodeDependencies && viewItem == dependency"
      }
    ]
  }
}

!> 注意:如果你需要针对特定的条目显示特殊的操作,定义树视图项的TreeItem.contextValue,并且在when中使用表达式,视图项的值储存在表达式的viewItem中。

如:

"contributes": {
  "menus": {
    "view/item/context": [
      {
        "command": "nodeDependencies.deleteEntry",
        "when": "view == nodeDependencies && viewItem == dependency"
      }
    ]
  }
}

为树视图提供数据


插件创作者需要注册TreeDataProvider,以便动态生成视图中的数据。

vscode.window.registerTreeDataProvider('nodeDependencies', new DepNodeProvider());

更多实现请参考nodeDependencies.ts

动态创建树视图


如果你想在视图中通过编程手段创建一些操作,你就不能再注册window.registerTreeDataProvider了,而是window.createTreeView,这样一来你就有权限提供你喜欢的视图操作了:

vscode.window.createTreeView('ftpExplorer', {  treeDataProvider: new FtpTreeDataProvider()});

更多实现请参考ftpExplorer.ts

Webview API

webview API为开发者提供了完全自定义视图的能力,例如内置的Markdown插件使用了webview渲染Markdown预览文件。Webview也能用于构建比VS Code原生API支持构建的更加复杂的用户交互界面。

可以把webview看成是VS Code中的iframe,它可以渲染几乎全部的HTML内容,它通过消息机制和插件通信。这样的自由度令我们的webview非常强劲并将插件的潜力提升到了新的高度。

相关链接


使用的VS Code API

我应该用webview吗?


webview虽然很赞,但是我们应该节制地使用这个功能——比如当VS Code原生API不够用时。Webview重度依赖资源,所以它脱离插件的进程而单独运行在其他环境中。在VS Code中使用设计不良的webview会让用户抓狂。

在使用webview之前,请作以下考虑:

  • 这个功能真的需要VS Code来提供吗?分离成一个应用或者网站会不会更好?
  • webview是实现这个特性的最后方案吗?VS Code原生API是否能达到同样的目的呢?
  • 你的webview所牺牲的高资源占用是否能换得同样的用户价值?

请记住:不要因为能使用webview而滥用webview。相反,如果你有充足的理由和自信,那么本篇教程对你来说会非常有用,现在就让我们开始吧。

Webviews API 基础


为了解释webviewAPI,我们先构建一个简单的Cat Coding插件。这个插件会用一个webview显示猫写代码的gif。随着我们不断了解API,我们会不断地给插件添加功能,包括我们的猫写了多少行代码的计数跟踪器,如果猫猫写出了bug还会有一个提示弹出框。

这是Cat Coding插件的第一版package.json,你可以在这里找到完整的代码。我们的第一版插件提供了一个命令,叫做catCoding.start。当用户从命令面板调用Cat Coding: Start new cat coding session,或者一个创建好的键绑定命令,我们的猫猫会出现在webview窗口内。

{
  "name": "cat-coding",
  "description": "Cat Coding",
  "version": "0.0.1",
  "publisher": "bierner",
  "engines": {
    "vscode": "^1.23.0"
  },
  "activationEvents": ["onCommand:catCoding.start"],
  "main": "./out/src/extension",
  "contributes": {
    "commands": [
      {
        "command": "catCoding.start",
        "title": "Start new cat coding session",
        "category": "Cat Coding"
      }
    ]
  },
  "scripts": {
    "vscode:prepublish": "tsc -p ./",
    "compile": "tsc -watch -p ./",
    "postinstall": "node ./node_modules/vscode/bin/install"
  },
  "dependencies": {
    "vscode": "*"
  },
  "devDependencies": {
    "@types/node": "^9.4.6",
    "typescript": "^2.8.3"
  }
}

现在让我们实现catCoding.start命令,在我们的主文件中,像下面这样注册一个基础的webview:

import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      // 创建并显示新的webview
      const panel = vscode.window.createWebviewPanel(
        'catCoding', // 只供内部使用,这个webview的标识
        'Cat Coding', // 给用户显示的面板标题
        vscode.ViewColumn.One, // 给新的webview面板一个编辑器视图
        {} // Webview选项。我们稍后会用上
      );
    })
  );
}

vscode.window.createWebviewPanel函数创建并在编辑区展示了一个webview,下图显示了如果你试着运行catCoding.start命令会显示的东西:

我们的命令以正确的标题打开了一个新的webview面板,但是没有任何内容!要想把我们的猫加到这个面板里面,我们需要webview.html设置HTML内容。

import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      // 创建和显示webview
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      // 设置HTML内容
      panel.webview.html = getWebviewContent();
    })
  );
}
function getWebviewContent() {
  return
    `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Cat Coding</title>
        </head>
        <body>
            <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
        </body>
        </html>
    `;
}

如果你再次运行命令,应该能看到下图:

大功告成!

webview.html应该是一个完整的HTML文档。使用HTML片段或者格式错乱的HTML会造成异常。

更新webview内容

webview.html也能在webview创建后更新内容,让我们用猫猫轮播图使Cat Coding具有动态性:

import * as vscode from 'vscode';
const cats = {
  'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
};
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      let iteration = 0;
      const updateWebview = () => {
        const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
        panel.title = cat;
        panel.webview.html = getWebviewContent(cat);
      };
      // 设置初始化内容
      updateWebview();
      // 每秒更新内容
      setInterval(updateWebview, 1000);
    })
  );
}
function getWebviewContent(cat: keyof typeof cats) {
  return
    `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Cat Coding</title>
        </head>
        <body>
            <img src="${cats[cat]}" width="300" />
        </body>
        </html>
    `;
}

因为webview.html方法替换了整个webview内容,页面看起来像重新加载了一个iframe。记住:如果你在webview中使用了脚本,那就意味着webview.html的重置会使脚本状态重置。

上述示例也使用了webview.title改变编辑器中的展示的文件名称,设置标题不会使webview重载。

生命周期

webview从属于创建他们的插件,插件必须保持住从webview返回的createWebviewPanel。如果你的插件失去了这个关联,它就不能再访问webview了,不过即使这样,webview还会继续展示在VS Code中。

因为webview是一个文本编辑器视图,所以用户可以随时关闭webview。当用户关闭了webview面板后,webview就被销毁了。在我们的例子中,销毁webview时抛出了一个异常,说明我们上面的示例中使用的seInterval实际上产生了非常严重的Bug:如果用户关闭了面板,setInterval会继续触发,而且还会尝试更新panel.webview.html,这当然会抛出异常。喵星人可不喜欢异常,我们现在就来解决这个问题吧。

onDidDispose事件在webview被销毁时触发,我们在这个事件结束之后更新并释放webview资源。

import * as vscode from 'vscode';
const cats = {
  'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif'
};
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      let iteration = 0;
      const updateWebview = () => {
        const cat = iteration++ % 2 ? 'Compiling Cat' : 'Coding Cat';
        panel.title = cat;
        panel.webview.html = getWebviewContent(cat);
      };
      updateWebview();
      const interval = setInterval(updateWebview, 1000);
      panel.onDidDispose(
        () => {
          // 当面板关闭时,取消webview内容之后的更新
          clearInterval(interval);
        },
        null,
        context.subscriptions
      );
    })
  );
}

插件也可以通过编程方式关闭webview视图——调用它们的dispose()方法。我们假设,现在限制我们的猫猫每天工作5秒钟:

export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      panel.webview.html = getWebviewContent(cats['Coding Cat']);
      // 5秒后,程序性地关闭webview面板
      const timeout = setTimeout(() => panel.dispose(), 5000);
      panel.onDidDispose(
        () => {
          // 在第五秒结束之前处理用户手动的关闭动作
          clearTimeout(timeout);
        },
        null,
        context.subscriptions
      );
    })
  );
}

移动和可见性

当webview面板被移动到了非激活标签上,它就隐藏起来了。但这时并不是销毁,当重新激活标签后,VS Code会从webview.html自动恢复webview的内容。

.visible属性告诉你当前webview面板是否是可见的。

插件也可以通过调用reveal()方法,程序性地将webview面板激活。这个方法可以接受一个用于放置面板的目标视图布局。一个面板一次只能显示在一个编辑布局中。调用reveal()或者拖动webview面板到新的编辑布局中去。

现在更新我们的插件,一次只允许存在一个webview视图。如果面板处于非激活状态,那catCoding.start命令就把这个面板激活。

export function activate(context: vscode.ExtensionContext) {
  // 追踪当前webview面板
  let currentPanel: vscode.WebviewPanel | undefined = undefined;
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const columnToShowIn = vscode.window.activeTextEditor
        ? vscode.window.activeTextEditor.viewColumn
        : undefined;
      if (currentPanel) {
        // 如果我们已经有了一个面板,那就把它显示到目标列布局中
        currentPanel.reveal(columnToShowIn);
      } else {
        // 不然,创建一个新面板
        currentPanel = vscode.window.createWebviewPanel(
          'catCoding',
          'Cat Coding',
          columnToShowIn,
          {}
        );
        currentPanel.webview.html = getWebviewContent(cats['Coding Cat']);
        // 当前面板被关闭后重置
        currentPanel.onDidDispose(
          () => {
            currentPanel = undefined;
          },
          null,
          context.subscriptions
        );
      }
    })
  );
}

下面是一个新插件的行为:

不论何时,如果webview的可见性改变了,或者当webview移动到了新的视图布局中,就会触发onDidChangeViewState。我们的插件可以利用这个时间改变布局中的webview显示的猫:

const cats = {
  'Coding Cat': 'https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif',
  'Compiling Cat': 'https://media.giphy.com/media/mlvseq9yvZhba/giphy.gif',
  'Testing Cat': 'https://media.giphy.com/media/3oriO0OEd9QIDdllqo/giphy.gif'
};
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      panel.webview.html = getWebviewContent(cats['Coding Cat']);
      // 根据视图状态变动更新内容
      panel.onDidChangeViewState(
        e => {
          const panel = e.webviewPanel;
          switch (panel.viewColumn) {
            case vscode.ViewColumn.One:
              updateWebviewForCat(panel, 'Coding Cat');
              return;
            case vscode.ViewColumn.Two:
              updateWebviewForCat(panel, 'Compiling Cat');
              return;
            case vscode.ViewColumn.Three:
              updateWebviewForCat(panel, 'Testing Cat');
              return;
          }
        },
        null,
        context.subscriptions
      );
    })
  );
}
function updateWebviewForCat(panel: vscode.WebviewPanel, catName: keyof typeof cats) {
  panel.title = catName;
  panel.webview.html = getWebviewContent(cats[catName]);
}

检查和调试webviews

在命令面板中输入Developer: Toggle Developer Tools能帮助你调试webview。运行命令之后会为当前可见的webview加载一个devtool:

webview的内容是在webview文档中的一个iframe中的,用开发者工具检查和修改webview的DOM,在webview内调试脚本。

如果你用了webview开发者工具的console,确保你在Console面板左上角的下拉框里选中了当前激活窗体环境:

激活窗体环境是webview脚本执行的地方。

另外,Developer: Reload Webview命令会刷新所有已激活的webview。如果你需要重置一个webview的状态,这个命令会非常有用,或者你想要读取硬盘内容的webview更新一下,也可以使用这个方法。

加载本地内容


webview运行在独立的环境中,因此不能直接访问本地资源,这是出于安全性考虑的做法。这也意味着要想从你的插件中加载图片、样式等其他资源,或是从用户当前的工作区加载任何内容的话,你必须使用webview中的vscode-resource:协议。

vscode-resource:协议就像file:协议一样,不过它只允许访问本地文件。和file:一样的是,vscode-resource:只能从绝对路径中加载资源。

想想一下,我们想要从本地把喵喵们的gif打包进来,而不是从Giphy(国外出名的gif收集站)里加载进来。要想做到这点,我们首先给本地文件新建一个URI,然后用vscode-resource:协议更新这些URI:

import * as vscode from 'vscode';
import * as path from 'path';
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {}
      );
      // 获取磁盘上的资源路径
      const onDiskPath = vscode.Uri.file(
        path.join(context.extensionPath, 'media', 'cat.gif')
      );
      // 获取在webview中使用的特殊URI
      const catGifSrc = onDiskPath.with({ scheme: 'vscode-resource' });
      panel.webview.html = getWebviewContent(catGifSrc);
    })
  );
}

catGifSrc的值最后会像这样:

vscode-resource:/Users/toonces/projects/vscode-cat-coding/media/cat.gif

默认情况下,scode-resource:只能访问下列地址的资源:

  • 你的插件安装的目录
  • 用户当前激活的工作区

你也可以用data URI将资源直接嵌套到webview中去。

控制本地资源访问

使用localResourceRoots选项,webview可以控制vscode-resource:加载的的资源。 localResourceRoots定义了可能被加载的本地内容的根URI。

我们用localResourceRoots去约束Cat Codingwebview只加载我们插件的media目录下的内容:

import * as vscode from 'vscode';
import * as path from 'path';
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {
          // 只允许webview加载我们插件的`media`目录下的资源
          localResourceRoots: [vscode.Uri.file(path.join(context.extensionPath, 'media'))]
        }
      );
      const onDiskPath = vscode.Uri.file(
        path.join(context.extensionPath, 'media', 'cat.gif')
      );
      const catGifSrc = onDiskPath.with({ scheme: 'vscode-resource' });
      panel.webview.html = getWebviewContent(catGifSrc);
    })
  );
}

为了禁止所有的本地资源,只要把localResourceRoots设为[]就好了。

通常来说,webview应该和加载本地资源一样严格,然而,vscode-resourcelocalResourceRoots并不保证百分百的安全性。请确保你的webview遵循安全性最佳实践,强烈建议考虑添加一个内容安全政策以便约束之后加载的内容。

给webview内容加上主题

webview可以基于当前的VS Code主题和CSS改变自身的样式。VS Code将主题分成3中类别,而且在body元素上加上了特殊类名以表明当前主题:

  • vscode-light——亮色主题
  • vscode-dark——暗色主题
  • vscode-high-contrast——高反差主题

下列CSS改变了基于用户当前主题的webview字体颜色:

body.vscode-light {
  color: black;
}
body.vscode-dark {
  color: white;
}
body.vscode-high-contrast {
  color: red;
}

当开发一个webview应用的时候,请保证应用能在三种主题下都可以运作,务必在高反差模式下测试你的webview,以便有视觉障碍的用户也能正常使用。

webview可以通过CSS variables访问VS Code主题,这些变量以vscode为前缀,并且用-替代了.,例如editor.foreground变成了var(--vscode-editor-foreground)

code {
  color: var(--vscode-editor-foreground);
}

更多可用的主题变量,参阅主题色彩。

下面也定义了一些与字体有关的变量:

  • -vscode-editor-font-family - 编辑器的文字类型(设置中的editor.fontFamily配置项)
  • -vscode-editor-font-weight - 编辑器的文字粗细(设置中的editor.fontWeight配置项)
  • -vscode-editor-font-size - 编辑器文字大小(设置中的editor.fontSize配置项)

脚本和信息传递


既然webview就像iframe一样,也就是说它们也可以运行脚本,webview中的Javascript默认是禁用的,不过我们能用enableScripts: true打开它。

让我们写一段脚本,追踪我们家喵星人写代码的行数。运行一个基础脚本非常的容易,但是注意这个示例只作演示用途,在实践中,你的webview应该遵循内容安全政策,禁止行内脚本。

import * as path from 'path';
import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
    context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
        const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
            // 在webview中启用脚本
            enableScripts: true
        });
        panel.webview.html = getWebviewContent();
    }));
}
function getWebviewContent() {
    return
        `
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Cat Coding</title>
            </head>
            <body>
                <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
                <h1 id="lines-of-code-counter">0</h1>
                <script>
                    const counter = document.getElementById('lines-of-code-counter');
                    let count = 0;
                    setInterval(() => {
                        counter.textContent = count++;
                    }, 100);
                </script>
            </body>
            </html>
        `;
}

哇!真是位高产的喵主子!

!> webveiw的脚本能做到任何普通网页脚本能做到的事情,但是webview运行在自己的上下文中,脚本不能访问VS Code API。

将插件的信息传递到webview

插件可以用webview.postMessage()将数据发送到它的webview中。这个方法能发送任何序列化的JSON数据到webview中,在webview中则通过message事件接受信息。

我们现在就来看看这个实现,在Cat Coding中新增一个命令来表示我们家的喵在重构他的代码(所以会减少代码总行数)。新增catCoding.doRefactor命令,利用postMessage把指示发送到webview中,webview中的window.addEventListener('message' event => { ... })则会处理这些信息:

export function activate(context: vscode.ExtensionContext) {
    // 现在只有一只喵喵程序员了
    let currentPanel: vscode.WebviewPanel | undefined = undefined;
    context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
        if (currentPanel) {
            currentPanel.reveal(vscode.ViewColumn.One);
        } else {
            currentPanel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
                enableScripts: true
            });
            currentPanel.webview.html = getWebviewContent();
            currentPanel.onDidDispose(() => { currentPanel = undefined; }, undefined, context.subscriptions);
        }
    }));
    // 我们新的命令
    context.subscriptions.push(vscode.commands.registerCommand('catCoding.doRefactor', () => {
        if (!currentPanel) {
            return;
        }
        // 把信息发送到webview
        // 你可以发送任何序列化的JSON数据
        currentPanel.webview.postMessage({ command: 'refactor' });
    }));
}
function getWebviewContent() {
    return
        `
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Cat Coding</title>
            </head>
            <body>
                <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
                <h1 id="lines-of-code-counter">0</h1>
                <script>
                    const counter = document.getElementById('lines-of-code-counter');
                    let count = 0;
                    setInterval(() => {
                        counter.textContent = count++;
                    }, 100);
                    // Handle the message inside the webview
                    window.addEventListener('message', event => {
                        const message = event.data; // The JSON data our extension sent
                        switch (message.command) {
                            case 'refactor':
                                count = Math.ceil(count * 0.5);
                                counter.textContent = count;
                                break;
                        }
                    });
                </script>
            </body>
            </html>
        `;

将webview的信息传递到插件中

webview也可以把信息传递回对应的插件中,用VS Code API 为webview提供的postMessage函数我们就可以完成这个目标。调用webview中的acquireVsCodeApi获取VS Code API对象。这个函数在一个会话中只能调用一次,你必须保持住这个方法返回的VS Code API实例,然后再转交到需要调用这个实例的地方。

现在我们在Cat Coding添加一个弹出bug的警示框:

export function activate(context: vscode.ExtensionContext) {
    context.subscriptions.push(vscode.commands.registerCommand('catCoding.start', () => {
        const panel = vscode.window.createWebviewPanel('catCoding', "Cat Coding", vscode.ViewColumn.One, {
            enableScripts: true
        });
        panel.webview.html = getWebviewContent();
        // 处理webview中的信息
        panel.webview.onDidReceiveMessage(message => {
            switch (message.command) {
                case 'alert':
                    vscode.window.showErrorMessage(message.text);
                    return;
            }
        }, undefined, context.subscriptions);
    }));
}
function getWebviewContent() {
    return
        `
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Cat Coding</title>
            </head>
            <body>
                <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
                <h1 id="lines-of-code-counter">0</h1>
                <script>
                    (function() {
                        const vscode = acquireVsCodeApi();
                        const counter = document.getElementById('lines-of-code-counter');
                        let count = 0;
                        setInterval(() => {
                            counter.textContent = count++;
                            // Alert the extension when our cat introduces a bug
                            if (Math.random() < 0.001 * count) {
                                vscode.postMessage({
                                    command: 'alert',
                                    text: '🐛  on line ' + count
                                })
                            }
                        }, 100);
                    }())
                </script>
            </body>
            </html>
        `;
}

出于安全性考虑,你必须保证VS Code API的私有性,也不会泄露到全局状态中去。

安全性


每一个你创建的webview都必须遵循这些基础的安全性最佳实践。

限制能力

webview应该留有它所需的最小功能集合即可。例如:如果你的webview不需要运行脚本,就不要设置enableScripts: true。如果你的webview不需要加载用户工作区的资源,就把localResourceRoots设置为[vscode.Uri.file(extensionContext.extensionPath)]或者[]以便禁止访问任何本地资源。

内容安全策略

内容安全策略可以进一步限制webview可以加载和执行的内容。例如:内容安全策略强制可以运行在webview中的脚本白名单,或者告诉webview只加载带https协议的图片。

要想加上内容安全策略,将<meta http-equiv="Content-Security-Policy">指令放到webview的<head>

function getWebviewContent() {
    return
        `
            <!DOCTYPE html>
            <html lang="en">
            <head>
                <meta charset="UTF-8">
                <meta http-equiv="Content-Security-Policy" content="default-src 'none';">
                <meta name="viewport" content="width=device-width, initial-scale=1.0">
                <title>Cat Coding</title>
            </head>
            <body>
                ...
            </body>
            </html>
        `;
}

default-src 'none';策略直接禁止了所有内容。我们可以按插件需要的最少内容修改这个指令,如只允许通过https加载本地脚本、样式和图片:

<meta
    http-equiv="Content-Security-Policy"
    content="default-src 'none'; img-src vscode-resource: https:; script-src vscode-resource:; style-src vscode-resource:;"
>

上述策略也隐式地禁用了内联脚本和样式。把内联样式和脚本提取到外部文件中是一个非常好的实践,也不会与内容安全策略冲突。

只通过https加载内容

如果你的webview允许加载外部资源,我们强烈建议你只允许通过https加载而不要使用http,上面的例子已经用内容安全策略展示了使用https的方式。

审查用户输入

就像构建普通HTML页面一样,你也同样需要在webview中审查用户输入的内容。 没有审查输入内容可能会导致内容注入,也就意味着将用户置于了危险之中。

可能需要审查的值:

  • 文件内容
  • 文件和文件夹路径
  • 用户工作区设置

可以考虑用一个辅助库去构建HTML模板,或者确保所有来自用户工作区的内容都通过了审查

只依赖审查内容的安全性是不够的,你也要遵循其他安全性的最佳实践,尽可能减少潜在的内容注入。

持久性


在webview的标准生命周期中,createWebviewPanel负责创建和销毁(用户关闭或者调用.dispose()方法)webview。而webview的内容再是在webview可见时创建的,在webview处于非激活状态时销毁。webview处于非激活标签中时,任何webview中的保留的状态都会丢失。

所以最好减少webview中的状态,取而代之用消息传递储存状态。

getState和setState

运行在webview中的脚本可以使用getStatesetState方法保存和恢复JSON序列化的状态对象。这个状态可以一直保留,即使webview面板已经被隐藏,只有当它销毁时,状态则会一起销毁。

// webview中的脚本
const vscode = acquireVsCodeApi();
const counter = document.getElementById('lines-of-code-counter');
// 检查是否需要恢复状态
const previousState = vscode.getState();
let count = previousState ? previousState.count : 0;
counter.textContent = count;
setInterval(() => {
    counter.textContent = count++;
    // 更新已经保存的状态
    vscode.setState({ count })
}, 100);

getStatesetState是用来保存状态的比较好的办法,因为他们的性能消耗要远低于retainContextWhenHidden

序列化

使用WebviewPanelSerializer之后,你的webview可以在VS Code关闭后自动恢复。序列化构建于getStatesetState之上,只有你的插件注册了WebviewPanelSerializer,这个功能才会生效。

给插件的package.json添加一个onWebviewPanel激活事件,然后我们的代码喵就能在VS Code重启后继续工作了:

"activationEvents": [
    ...,
    "onWebviewPanel:catCoding"
]

这个激活事件确保我们的插件不论VS Code何时恢复catCodingwebview时都会启动。

然后在我们插件的activate方法中调用registerWebviewPanelSerializer注册一个新的WebviewPanelSerializer,这个函数负责恢复webview之前保存的内容。其中的state就是webview用setState设置的JSON格式的状态。

export function activate(context: vscode.ExtensionContext) {
    // 常见设置...
    // 确保我们注册了一个序列化器
    vscode.window.registerWebviewPanelSerializer('catCoding', new CatCodingSerializer());
}
class CatCodingSerializer implements vscode.WebviewPanelSerializer {
    async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: any) {
        // `state`是webview内调用`setState`保留住的
        console.log(`Got state: ${state}`);
        // 恢复我们的webview内容
        //
        // 确保我们将`webviewPanel`传递到了这里
        // 然后用事件侦听器恢复我们的内容
        webviewPanel.webview.html = getWebviewContent();
    }
}

在VS Code中打开一个喵喵打代码的面板,关闭后重启就能看到这个面板恢复到了之前的状态和位置。

隐藏时保留上下文

如果webview的视图非常复杂,或者状态不能很快地保存和恢复,你则可以用retainContextWhenHidden选项,这个选项在不可见的状态中保存了webview的内容,即使webview本身不处于激活状态。

虽然Cat Coding说不上有很复杂的状态,不过我们可以打开retainContextWhenHidden看看webview的行为会发生什么变化:

import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('catCoding.start', () => {
      const panel = vscode.window.createWebviewPanel(
        'catCoding',
        'Cat Coding',
        vscode.ViewColumn.One,
        {
          enableScripts: true,
          retainContextWhenHidden: true
        }
      );
      panel.webview.html = getWebviewContent();
    })
  );
}
function getWebviewContent() {
  return
    `
        <!DOCTYPE html>
        <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title>Cat Coding</title>
        </head>
        <body>
            <img src="https://media.giphy.com/media/JIX9t2j0ZTN9S/giphy.gif" width="300" />
            <h1 id="lines-of-code-counter">0</h1>
            <script>
                const counter = document.getElementById('lines-of-code-counter');
                let count = 0;
                setInterval(() => {
                    counter.textContent = count++;
                }, 100);
            </script>
        </body>
        </html>
    `;
}

我们可以注意到计数器没有重置,webview隐藏之后就恢复了。而且不需要多余的代码!retainContextWhenHidden的行为就像浏览器一样,脚本和其他内容被暂时挂起,但是一旦webview可见之后就会立即恢复。但是在webview隐藏状态下,你还是不能给它发送消息的。

虽然retainContextWhenHidden很吸引人,但是记住这个功能的内容占用很高,只有其他的持久化技术无能为力之时再选择这种方式。

虚拟文档

通过VS Code的文本内容供应器API(text document content provider API),你可以为任意来源的文件创建只读文档。本示例源码请查看https://github.com/Microsoft/vscode-extension-samples/blob/master/virtual-document-sample/README.md

Text Document Content Provider


这个API工作于uri协议之上,你需要声明一个供应器函数(provider),然后这个函数还需要返回文本内容。供应器函数必须提供协议(scheme),而且函数注册之后不可改变这个协议。一个供应器函数可以对应多个协议,而多个供应器函数也可以只注册一个协议。

vscode.workspace.registerTextDocumentContentProvider(myScheme, myProvider);

调用registerTextDocumentContentProvider函数会返回一个用于释放资源的释放器。供应器函数还必须实现provideTextDocumentContent函数,这个函数需要传入uri参数和取消式令牌(cancellation token)调用。

const myProvider = class implements vscode.TextDocumentContentProvider {
    provideTextDocumentContent(uri: vscode.Uri): string {
        // 简单调用cowsay, 直接把uri-path当做文本内容
        return cowsay.say({ text: uri.path });
    }
};

!> 注意:我们的供应器函数不为虚拟文档创建uri——他的角色仅仅只是根据uri返回对应的文本内容

下面我们简单使用一个’cowsay’命令创建一个uri,然后编辑器就能显示了:

vscode.commands.registerCommand('cowsay.say', async () => {
    let what = await vscode.window.showInputBox({ placeHolder: 'cow say?' });
    if (what) {
        let uri = vscode.Uri.parse('cowsay:' + what);
        let doc = await vscode.workspace.openTextDocument(uri); // 调用供应器函数
        await vscode.window.showTextDocument(doc, { preview: false });
    }
});

这个命令首先弹出了一个输入框,然后创建了一个cowsay协议的uri,再然后根据这个uri读取了文档,最后为这个文档内容打开了一个编辑器(这里的“编辑器”不是指VS Code本身,而是VS Code中打开的单个编辑区tab)。在第三步中,供应器函数需要为这个uri提供对应的内容。

经过这整个流程,我们才算完整地实现了一个文本内容供应器,接下来的部分我们继续学习怎么更新虚拟文档,怎么注册虚拟文档的 UI命令。

更新虚拟文档

为了支持跟踪虚拟文档发生的变化,供应器实现了onDidChange事件。如果文档正在被使用,那么必须为其提供一个uri来调用它,同时编辑器会请求新的内容。

vscode.Event定义了VS Code的事件规范。实现事件的最好方式就是使用vscode.EventEmitter,比如:

const myProvider = class implements vscode.TextDocumentContentProvider {
  // 事件发射器和事件
  onDidChangeEmitter = new vscode.EventEmitter<vscode.Uri>();
  onDidChange = this.onDidChangeEmitter.event;
  //...
};

上述就是VS Code监听虚拟文档变化所必须的内容。下面将使用事件发射器来添加编辑器行为

添加编辑器命令

为了阐述事件变动和获取更多cowsay,我们需要倒叙cow刚刚说的东西。首先我们需要一个命令:

// 注册一个可以更新当前cow的命令
subscriptions.push(
    vscode.commands.registerCommand('cowsay.backwards', async () => {
        if (!vscode.window.activeTextEditor) {
            return; // 不打开编辑器
        }
        let { document } = vscode.window.activeTextEditor;
        if (document.uri.scheme !== myScheme) {
            return; // 不是我的协议时直接返回
        }
        // 获取path的内容, 对这个内容倒序处理, 然后创建一个新的uri
        let say = document.uri.path;
        let newSay = say
            .split('')
            .reverse()
            .join('');
        let newUri = document.uri.with({ path: newSay });
        await vscode.window.showTextDocument(newUri, { preview: false });
    })
);

上面的代码片段检查了我们当前是不是激活了一个编辑器(用户当前选中的编辑器tab),对应的文档是不是符合我们的协议。因为命令是对任何人生效的,所以我们有必要去做这些检查。然后uri的path对象被翻转,再重新创建出一个新的uri,最后则打开了一个编辑器。

注册命令最重要事就是在package.json中声明配置。我们在contributes部分添加下列配置:

"menus": {
  "editor/title": [
    {
      "command": "cowsay.backwards",
      "group": "navigation",
      "when": "resourceScheme == cowsay"
    }
  ]
}

contributes/commands中的cowsay.backwards命令告诉编辑器操作出现在编辑器的标题菜单中(工具栏右上角),但如果只是这样简单的配置,每个编辑器就都会显示这个命令。然后我们的when语句就出场了,它描述了何时才显示这个操作。在这个例子中,文档的资源协议必须是cowsay,我们的命令才会生效。这个配置对默认显示全部命令的commandPalette菜单同样生效。

事件的可见性

文档供应器函数是VS Code中的一等公民,它们的内容以常规的文本文档格式呈现,它们共用一套基础实现方式——如:使用了文件系统的实现。这也就意味着“你的”文档无法被隐藏,它们必定会出现在onDidOpenTextDocumentonDidCloseTextDocument事件中,它们是vscode.workspace.textDocuments中的一部分。通用的准则就是根据文档的协议决定你是否需要对文档进行什么操作。

文件系统API

如果你需要更强的灵活性和掌控力,请查看FileSystemProviderAPI,它可以实现整套完整的文件系统,获取文件、文件夹、二进制数据,删除文件,创建文件等等。

任务

通常,在VS Code中,用户可以通过task.json定义一个任务。不过在软件开发中,VS Code会自动检测某些任务。

本节介绍了插件应该怎样使用Rakefiles中的自动检测任务配置项,为最终用户提供任务。完整的源代码请参阅这里

定义任务


想要定义一个系统级别的任务,插件需要通过properties定义任务,在下面叫做Rake的例子中,任务是这样定义的:

?>译者注:rake是ruby实现的任务管理和自动构建工具,详细请参考rake

"taskDefinitions": [
    {
        "type": "rake",
        "required": [
            "task"
        ],
        "properties": {
            "task": {
                "type": "string",
                "description": "The Rake task to customize"
            },
            "file": {
                "type": "string",
                "description": "The Rake file that provides the task. Can be omitted."
            }
        }
    }
]

上面代码里面,我们为rake任务集配置了一个任务定义。任务定义有两个属性taskfiletask是Rake任务的名字,file指向了包含任务的文件。task属性是必须的,file则为可选。如果省略了file属性,则会使用工作区根目录下名为RakeFile的文件。

任务供应器函数


和语言供应器函数相同,任务供应器使插件支持代码补全,一个插件可以只注册一个任务供应器函数然后执行所有可用的任务集合。使用vscode.tasks命名空间达成这一目标:

import * as vscode from 'vscode';
let rakePromise: Thenable<vscode.Task[]> | undefined = undefined;
const taskProvider = vscode.tasks.registerTaskProvider('rake', {
    provideTasks: () => {
        if (!rakePromise) {
            rakePromise = getRakeTasks();
        }
        return rakePromise;
    },
    resolveTask(_task: vscode.Task): vscode.Task | undefined {
        return undefined;
    }
});

目前resolveTask只返回了undefined,而将来VS Code会通过这个方法优化任务的加载。

getRakeTasks的实现做了下面的事情:

  • 使用rake -AT -f Rakefile命令列出rake文件中的所有rake任务
  • 转换为stdio输出
  • 对每个任务创建一个vscode.task实现

因为一个rake任务初始化需要package.json中有对应的任务定义,VS Code会用TypeScript接口定义出结构,像这样:

interface RakeTaskDefinition extends vscode.TaskDefinition {
    /**
     * The task name
     */
    task: string;
    /**
     * The rake file containing the task
     */
    file?: string;
}

假设我们的输出最终来自于一个叫compile的任务,那么对应的任务创建过程如下所示:

let task = new vscode.Task(
    { type: 'rake', task: 'compile' },
    'compile',
    'rake',
    new vscode.ShellExecution('rake compile')
);

每个输出任务都对应着上述过程,最后通过调用getRakeTasks会返回一个任务数组。

ShellExecution会针对不同的系统在shell中执行rake compile命令(如:在Windows下会在PowerShell中执行,Ubuntu则是bash)。如果某个任务需要直接执行进程(不通过shell生成),则可以使用vscode.ProcessExecutionProcessExecution的优势在于插件可以完全控制传入进程的参数,ShellExecution则会使用shell命令转义(比如:bash中的*展开)。如果ShellExecution是通过单个命令创建的,那么插件需要在命令内部确保引号和转义符的正确使用(比如,如何处理空格)。

源控制API

VS Code 允许插件创作者通过扩展API去定义源控制管理特性(Source Control Management,SCM),VS Code整合了各式各样的SCM体系,而只给用户展现了一组小巧、强大的API接口,还是带用户界面的那种。

VS Code自带一个源控制器:Git,它是源控制API的最佳实践。如果你想构建你自己的SCM供应器,那么这是一个很好的起点

VS Code插件市场还有很多类似的超赞的插件,比如SVN

如果你需要帮助,请查看vscode命名空间API

源控制模型


SourceControl负责生产源控制模型的实体,它里面有SourceControlResourceState实例的资源状态,而资源状态又是SourceControlResourceGroup实例整理成的。

通过vscode.scm.createSourceControl创建一个新的源控制器

为了更好地理解这几种实体的交互,让我们拿Git来做例子,考虑下列git status输出:

vsce master* → git status
On branch master
Your branch is up-to-date with 'origin/master'.
Changes to be committed:
  (use "git reset HEAD <file>..." to unstage)
        modified:   README.md
        renamed:    src/api.ts -> src/test/api.ts
Changes not staged for commit:
  (use "git add/rm <file>..." to update what will be committed)
  (use "git checkout -- <file>..." to discard changes in working directory)
        deleted:    .travis.yml
        modified:   README.md

这个工作区里面发生了很多事,首先,README.md文件已经被修改了但还没有提交,然后立刻又被修改了。 其次,src/api.ts文件被移动到了src/test/api.ts,这个修改已经存备(staged), 最后,.travis.yml文件被删除。

对于这个工作区而言,Git定义了两个资源组:工作中(Working tree)已索引(Index),对于单个组而言,每次文件修改就会产生一些资源状态:

  • 已索引

    - 资源组

    • 修改README.md - 资源状态
    • 移动src/api.tssrc/test/api.ts - 资源状态
  • 工作中

    - 资源组

    • 删除.travis.yml - 资源状态
    • 修改README.md - 资源状态

同一个README.md是怎么成为两组截然不同的资源状态呢?

下面揭秘Git是如何创建这个模型的:

function createResourceUri(relativePath: string): vscode.Uri {
  const absolutePath = path.join(vscode.workspace.rootPath, relativePath);
  return vscode.Uri.file(absolutePath);
}
const gitSCM = vscode.scm.createSourceControl('git', "Git");
const index = gitSCM.createResourceGroup('index', "Index");
index.resourceStates = [
  { resourceUri: createResourceUri('README.md') },
  { resourceUri: createResourceUri('src/test/api.ts') }
];
const workingTree = gitSCM.createResourceGroup('workingTree', "Changes");
workingTree.resourceStates = [
  { resourceUri: createResourceUri('.travis.yml') },
  { resourceUri: createResourceUri('README.md') }
];

源变动和最终产生的资源组会传递到源控制视图上。

源控制视图


当源变动时,VS Code会生成源控制视图。源状态可通过SourceControlResourceDecorations自定义:

export interface SourceControlResourceState {
  readonly decorations?: SourceControlResourceDecorations;
}

上述例子已经足以让源控制视图生成一个简单的列表,不过用户可能想要在不同的资源状态上进行不同的操作。比如,当用户点击资源状态时,会发生什么呢?资源状态提供了一个可选命令去处理这类场景:

export interface SourceControlResourceState {
  readonly command?: Command;
}

菜单

要想提供更加丰富的交互效果,我们提供了5个源控制菜单项供你使用。

scm/title菜单在源控制视图的顶部右上方,菜单项水平排列在标题栏中,另外一些会在...下拉菜单中。

scm/resourceGroup/contextscm/resourceState/context是类似的,你可以通过前者自定义资源组,后者则是定义资源状态。将菜单项放在inline组里,可以水平在视图中展示它们。而其他的菜单项可以通过鼠标右击的形式展示在菜单中。菜单中调用的命令会传入资源状态作为参数。注意SCM视图提供多选,因此命令函数可能一次性会接收一个或多个参数。

例如,Git支持往scm/resourceState/context菜单中添加git.stage命令和使用下列方法,提供多个文件的存备(staged):

stage(...resourceStates: SourceControlResourceState[]): Promise<void>;

创建它们的时候,SourceControlSourceControlResourceGroup实例会需要你提供一个string类型的id,这些值最终会在scmProviderscmResourceGroup以上下文键值的形式出现。在菜单项的when语法中使用这些上下文键值。看个Git如何通过git.stage命令显示菜单项的:

{
  "command": "git.stage",
  "when": "scmProvider == git && scmResourceGroup == merge",
  "group": "inline"
}

scm/change/title可以对行内变动配置标题栏的命令(contribute commands to the title bar of an inline change)。命令中的参数有文档的URI,变动数组,当前行内变动所在索引。例如下面是一个可以配置菜单的GitstageChange命令声明:

async stageChange(uri: Uri, changes: LineChange[], index: number): Promise<void>;

scm/sourceControl菜单根据环境出现在源控制实例的边上。

最后,scm/change/title菜单是和快速Diff功能相关联的,越新的文件越靠前,你可以针对特定的代码变动调用命令。

SCM 输入框

源控制输入框位于每个源控制视图的顶部,接收用户输入的信息。你可以获取(或设置)这个信息供后续使用。在Git中,比如说,这可以作为一个commit框,用户输入了提交信息后,触发git commit命令:

export interface SourceControlInputBox {
  value: string;
}
export interface SourceControl {
  readonly inputBox: SourceControlInputBox;
}

用户可以通过Ctrl+Enter(Mac上是Cmd+Enter)接收任意信息,在SourceControl中的acceptInputCommand处理这类事件。

export interface SourceControl {
  readonly acceptInputCommand?: Command;
}

快速Diff


VS Code支持显示快速Diff编辑器的高亮槽,点击这些槽会出现一个内部diff交互器,你可以在这里为上下文配置命令。

这些高亮槽是VS Code自己计算出来的,你要做的就是根据给定的文件提供原始文件内容

export interface SourceControl {
  readonly acceptInputCommand?: Command;
}

使用QuickDiffProvider,你的实现需要告诉VS Code——参数传入的给定资源Uri所对应的原始资源Uri

?> 提示: 如果你想在给定Uri的情况下,为任意资源提供内容,那么你可以把源控制API工作区命名空间的registerTextDocumentContentProvider方法结合起来使用。

调试器插件

VS Code已经内置了一套通用的用户界面,插件作者能够通过VS Code的调试架构轻松将已有的调试器整合进来。

VS Code已经内置了一个Node.js调试器插件,它将成为你学习VS Code调试器特性的绝佳搭档。

上面的截图展示了以下调试功能:

  1. 管理调试器配置
  2. 开始、停止、步进等调试操作
  3. 源、函数、条件断点、行断点和记录点
  4. 支持多线程和多进程的调用栈
  5. 视图中浏览复杂的数据,鼠标悬停在数据上可以看到更多信息
  6. 鼠标悬停在源代码中可以看到变量的值
  7. 管理watch表达式
  8. 调试控制台支持交互操作,如求值、自动补全等

本节将帮你创建一个任意调试器都可以和VS Code协作的调试器插件。

VS Code 中的调试架构


VS Code基于抽象协议,实现了一个原生(非语言相关的)的调试器UI,它可以和任意后台调试程序通信。通常来讲,调试器不会实现这份协议,因此调试器中需要一些中间件去“适配”这个协议。这个中间件一般而言是一个独立和调试器通信的进程。

我们将这个中间件称为调试适配器(Debug Adapter)(简写为DA),在VS Code和DA之间通信的抽象协议称之为调试适配器协议(Debug Adapter Protocol) (简写DAP)。调试适配器协议独立于VS Code,它有自己的网站,你在上面可以找到相关的介绍和概述,以及详细的说明书,上面还列出了一些已知实现和支持工具,这份努力背后的故事和动机,我们都记录在了博客中。

因为调试适配器独立于VSCode,所以它可用在其他开发工具中,它们无需匹配VS Code的插件架构,而只需基于插件和发布内容配置即可。

出于这个原因,VS Code提供了一个配置点debuggers,调试适配器在这里可以配置特定的调试类型(例如:Node.js调试器使用node)。用户只要启动了这个类型的调试适配器会话,VS Code就能加载注册好的调试适配器。

因此调试适配器的最小形式就是声明一个配置,对应调试适配器的实现,这个插件就是调试适配器的装载容器,而且不需要任何多余的代码。

一个更贴近现实的调试器插件往往会添加很多配置,如下面的:

  • 调试器支持的语言。VS Code会为这些语言启用UI界面的断点功能
  • 由调试器引入的JSON格式的调试配置属性。VS Code会使用这个格式校验launch.json中的配置,并提供补全功能
  • 首次加载调试时,VS Code自动生成初始launch.json文件
  • 用户可以给launch.json添加的调试配置片段
  • 声明调试配置中可以使用的变量

模拟调试插件


由于从头开始创建一个调试适配器太繁琐了,所以我们将从简单的DA(我们已经创建过的入门级调试适配器)开始。因为它不与真正的调试器进行通信,所以就叫它——模拟调试吧。

模拟调试模拟了调试器功能,支持:

  • 单步调试
  • 跳到下一个断点
  • 断点
  • 异常
  • 访问变量

在深入了解开发中的模拟调试之前,我们先去VS Code插件市场安装个预构建版本玩一玩,就像下面这样:

  • 打开VS Code的插件面板,输入”mock”并找到Mock Debug插件
  • 安装重启

通过如下流程来启动模拟调试

  • 新建一个空的文件夹mock test并在VS Code中打开
  • 创建一个readme.md,在里面随便写点什么东西
  • 切换到调试视图,点一下齿轮图标
  • VS Code会让你选择一个”环境”,并将其作为默认的启动配置。这里选择”Mock Debug”。
  • 点击绿色的开始按钮,然后开始调试

至此,一个调试会话就开始了,你可以在readme.md文件中进行单步调试、打断点。如果某一行出现异常则会跳进该异常。

在使用模拟调试之前,我们建议你卸载掉预构建版本

  • 切换到Extensions视图,然后单击模拟调试插件的齿轮图标
  • 卸载该插件并重启VS Code

开发环境配置模拟调试


现在让我们下载Mock Debug的源码,然后用VS Code进行开发吧:

git clone https://github.com/Microsoft/vscode-mock-debug.git
cd vscode-mock-debug
npm install

用VS Code打开vscode-mock-debug项目

我们的项目里面有什么呢?

  • package.json

    是mock-debug插件的配置清单:

    • 里面是mock-debug插件的发布内容配置清单
    • compilewatch脚本会将Typescript源码编译到out文件夹中,然后watch脚本会追踪源码每个细微的修改
    • vscode-debugprotocolvscode-debugadaptervscode-debugadapter-testsupportnpm依赖包简化了基于node的调试适配器开发工作
  • src/mockRuntime.ts是一个模拟的运行时,仅仅包含一些简单的调试API

  • src/mockDebug.ts是我们的主要代码,是它将运行时适配到调试适配器上。你可以在里面找到各种处理DAP请求的方式。

  • 调试插件实现于调试适配器,所以你可以完全不使用创建普通插件的代码(比如:原来插件的代码运行在扩展主机环境中),但是mock-debug还是有个小小的src/extension.ts,这份代码里面阐释了调试器插件中插件部分的代码可以做些什么。

现在构建项目,然后加载Mock Debug插件。选择调试侧边栏,加载 Extension 配置,然后按下F5。接下来,会启动插件的Typescript编译工作,将转换后的代码输出到out文件夹中,然后进行全量编译,再接着,watcher任务会启动以便侦听你的改动。

代码编译完成后,带有”[Extension Development Host]”(中文环境下是”[扩展开发主机]”)VS Code新窗口会自动打开,Mock Debug插件就运行在调试模式中了。在这个窗口中,打开mock test项目,打开里面的readme.md,然后直接按下F5启动调试会话,现在你就可以调试了!

因为的你插件运行在 调试模式 中,所以你能在src/extension.ts里面打断点,不过就如上文所说,这个插件关于插件本身的代码是没有多少的,最有意思的代码运行在调试适配器里,它是一个独立的进程。

要想调试调试适配器本身,我们需要把它运行在调试模式里。最简单的办法就是将调试适配器以服务器模式运行,然后配置VS Code去连接它。在你的vscode-mock-debug项目中,重新在打开的调试侧边栏的配置下拉菜单中选择Server配置,按下旁边的绿色开始按钮。

因为我们已经启动了一个调试会话,所以VS Code 调试器UI现在会进入 多会话 模式,在调用栈(CALL STACK)视图中你现在可以看到2个调试会话—— ExtensionServer

现在我们可以同时调试插件和DA(调试适配器)了。到我们目前这一步还有个更快的方式,启动调试时选择Extension + Server配置就会自动加载这两个会话。

另外,调试插件和调试适配器更简单的方式会在下面说明。

src/mockDebug.ts中的launchRequest(...)最开始的地方打上断点,然后最后一步则是在你的mock test启动配置中添加debugServer属性和对应的端口值4711就完成了调试器和调试适配器的连接。

{
    "version": "0.2.0",
    "configurations": [
        {
            "type": "mock",
            "request": "launch",
            "name": "mock test",
            "program": "${workspaceFolder}/readme.md",
            "stopOnEntry": true,
            "debugServer": 4711
        }
    ]
}

如果你现在就加载这个调试配置,调试适配器不会以分离的进程启动,而是直接连接到已经存在的本地服务器端口4711上。现在你可以在launchRequest打断点了。

经过这样一连串的配置,你终于可以轻松地编辑、编译和调试Mock Debug插件了。

但是好戏才刚刚开始:你需要替换src/mockDebug.tssrc/mockRuntime.ts的中的调试适配器代码,让它可以和“真正的”调试器或者运行时通信。这项工作涉及到理解和实现调试适配器协议。

更多内容请查看这里

剖析调试器插件的package.json


除了提供调试适配的特定实现之外,调试器插件还需要一个配置各种各样和调试相关的package.json

所以下面我们进一步看看Mock Debug的package.json.

就像一般的VS Code插件,package.json声明了一些基础信息,如插件的namepublisherversion等。其中配置categories字段可以让你的插件更容易在插件市场中被其他人发现。

{
    "name": "mock-debug",
    "displayName": "Mock Debug",
    "version": "0.24.0",
    "publisher": "...",
    "description": "Starter extension for developing debug adapters for VS Code.",
    "author": {
        "name": "...",
        "email": "..."
    },
    "engines": {
        "vscode": "^1.17.0",
        "node": "^7.9.0"
    },
    "icon": "images/mock-debug-icon.png",
    "categories": ["Debuggers"],
    "contributes": {
        "breakpoints": [{ "language": "markdown" }],
        "debuggers": [
            {
                "type": "mock",
                "label": "Mock Debug",
                "program": "./out/mockDebug.js",
                "runtime": "node",
                "configurationAttributes": {
                    "launch": {
                        "required": ["program"],
                        "properties": {
                            "program": {
                                "type": "string",
                                "description": "Absolute path to a text file.",
                                "default": "${workspaceFolder}/${command:AskForProgramName}"
                            },
                            "stopOnEntry": {
                                "type": "boolean",
                                "description": "Automatically stop after launch.",
                                "default": true
                            }
                        }
                    }
                },
                "initialConfigurations": [
                    {
                        "type": "mock",
                        "request": "launch",
                        "name": "Ask for file name",
                        "program": "${workspaceFolder}/${command:AskForProgramName}",
                        "stopOnEntry": true
                    }
                ],
                "configurationSnippets": [
                    {
                        "label": "Mock Debug: Launch",
                        "description": "A new configuration for launching a mock debug program",
                        "body": {
                            "type": "mock",
                            "request": "launch",
                            "name": "${2:Launch Program}",
                            "program": "^\"\\${workspaceFolder}/${1:Program}\""
                        }
                    }
                ],
                "variables": {
                    "AskForProgramName": "extension.mock-debug.getProgramName"
                }
            }
        ]
    },
    "activationEvents": ["onDebug", "onCommand:extension.mock-debug.getProgramName"]
}

现在我们来看看调试器插件中的contributes部分。

首先,breakpoints配置部分列出了可以使用断点的语言列表,没有这个配置的话,就不可能在 Markdown文件中设置断点了。

接下来是debuggers部分,这里引入了一个类型是mock的调试器,用户可以在调试器加载配置中引用这个类型。可选属性label是这个调试器的名字,它会显示在UI上。

因为调试器插件使用了调试适配器,所以它的的关联路径得通过program属性配置。为了保证插件的自包含性(self-contained),这个应用必须在我们的插件文件夹中。按惯例,我们将这个应用放在out或者bin中,当然你也可以使用其他名称的文件夹存放。

因为VS Code运行在不同的平台上,我们需要确保DA程序也能够支持不同的平台。对于这点,我们提供了下列选项:

  1. 如果程序在平台上的实现都是各自独立的,比如:这个程序的运行时支持所有平台,你可以在runtime属性中指明。 到目前为止,VS Code支持nodemoni运行时,我们的Mock Debug就使用了这个方式。
  2. 如果你的DA在不同的平台上对应着不同的可执行程序,那么你可以这样使用program属性:
"debuggers": [{
    "type": "gdb",
    "windows": {
        "program": "./bin/gdbDebug.exe",
    },
    "osx": {
        "program": "./bin/gdbDebug.sh",
    },
    "linux": {
        "program": "./bin/gdbDebug.sh",
    }
}]
  1. 组合上面两种方式也是可以的。下面的例子实现了在macOS和Linux上使用同一个mono应用,但是Windows上就不是。
"debuggers": [{
    "type": "mono",
    "program": "./bin/monoDebug.exe",
    "osx": {
        "runtime": "mono"
    },
    "linux": {
        "runtime": "mono"
    }
}]

configurationAttributes声明了这个调试器的launch.json中的属性可以使用的协议。这个协议用于校验launch.json,同时支持编辑加载配置时的智能补全和悬停帮助。

initialConfigurations定义了这个调试器的初始launch.json。当一个项目没有launch.json,然后用户打开了调试会话时,就会使用这个启动配置。然后VS Code会让用户选择一个调试环境,接着再创建对应的launch.json

除了在package.json中静态定义launch.json的初始内容,你还可以使用DebugConfigurationProvider动态注入初始配置内容(详情见下使用DebugConfigurationProvider)。

configurationSnippets定义了编辑launch.json会为用户呈现的代码补全提示。同样,按约定label属性定义了调试环境的名称,所以当大量补全提示出现的时候,用户才能一眼认出自己想要的那个。

variables配置,将“变量”绑定到了“命令”上。这些变量会出现在加载配置(launch.json)中,用法是${command:xyz},调试会话启动后,其中的值会被命令中的返回值替换。

命令实现在插件(而不是调试适配器)中,它可以由一句简单的表达式实现,也可以复杂到基于插件API和UI特性实现。Mock Debug将变量AskForProgramName绑定到了命令extension.mock-debug.getProgramName,这个命令的实现src/extension.ts中,代码中的showInputBox允许用户为程序命名:

vscode.commands.registerCommand('extension.mock-debug.getProgramName', config => {
    return vscode.window.showInputBox({
        placeHolder: 'Please enter the name of a markdown file in the workspace folder',
        value: 'readme.md'
    });
});

现在加载配置(launch.json)中可以使用${command:AskForProgramName}中的值(文本类型)了。

使用DebugConfigurationProvider


如果你觉得package.json中和调试相关的发布内容配置不够你用,DebugConfigurationProvider可以动态控制调试插件下列方面的内容:

  • 动态生成launch.json中的配置。比如:根据工作区的信息生成一些配置。
  • 在启动新的调试会话前,解析(或修改)加载配置。有了这个功能,你可以根据工作区的不同填入对应的配置默认值。

src/extension.ts中的MockConfigurationProvider实现了resolveDebugConfiguration,它会检测调试会话启动时是不是还没有launch.json文件,而且Markdown文件已经打开了。这种场景非常常见,用户已经打开了文件,他想要立刻启动调试而且不想要搞任何配置。

通过vscode.debug.registerDebugConfigurationProvider注册调试配置供应器函数,它一般在插件的active函数中。DebugConfigurationProvider需要尽早注册,一旦调试功能被使用到了,插件就应该启动。我们通过package.json中的onDebug事件轻松搞定这个需求:

"activationEvents": [
    "onDebug",
    // ...
],

在低开销的插件启动时(启动时不会花太多时间),这个机制会如预期工作。但是如果插件的启动开销较大(比如启动一个语言服务器),那么onDebug事件可能会对其他调试插件产生副作用,因为onDebug事件已经激活了其他插件,但是其他插件因为阻塞还来不及接收到具体的调试类型。

对于高开销的调试插件来说,更好的方法就是使用粒度更细的 激活事件:

  • onDebugInitialConfigurations会在DebugConfigurationProviderprovideDebugConfigurations调用前触发
  • onDebugResolve:type会在DebugConfigurationProviderresolveDebugConfiguration取得具体的调试类型前触发

!> 首要原则:如果调试插件的开销很小,就用onDebug,根据DebugConfigurationProvider是否实现了provideDebugConfigurationsresolveDebugConfiguration,然后在对应的onDebugInitialConfigurations或者onDebugResolve中处理。

发布调试器插件


通过下面的步骤将你的调试适配器发布到市场上:

  • 更新package.json中的发布配置内容表明你调试适配器的功能和目标
  • 参考发布插件部分然后将你的插件上传到市场上

开发调试器插件的其他方式


如我们所见,开发一个调试插件涉及到一个普通插件再加上一个调试适配器,它们分别运行在不同的会话中。VS Code支持这样的实现,但是简单的办法是还是把插件和调试适配器用一个程序实现,这样你就可以在一个调试会话中同时调试了。

实际上,只要你的调试适配器是基于Typescript/Javascript实现的,这个方法就都是可行的。基本的思路是把调试适配器实现为一个服务器,让插件去启动这个服务,再让VS Code连接上去,这样你就不用每个调试会话都启动一个新的调试适配器了。

Mock Debug的例子阐述了一个DebugAdapterDescriptorFactory可以怎样创建和注册一个基于服务器的调试适配器。通过将编译时的EMBED_DEBUG_ADAPTER配置设置为true启用这个特性。现在如果你用F5启动调试,你就不仅仅是在插件开发主机中打了断点,你也同时在调试适配器中打了同样的断点。

Markdown插件

Markdown插件可以帮你扩展和加强VS Code内置的Markdown预览,包括改变预览的样式、添加新的Markdown语法。

用CSS改变Markdown预览样式


配置CSS可以改变markdown预览的布局和样式,在你的插件pacakge.json中注册markdown.previewStyles发布内容配置即可:

"contributes": {
    "markdown.previewStyles": [
        "./style.css"
    ]
}

markdown.previewStyles类型是插件根目录下的文件列表。

配置的样式会在用户的"markdown.styles"之前,内置Markdown预览样式之后加载。

Markdown Preview GitHub Styling是一个如何将Markdown预览变成像GitHub渲染风格的好例子,在GitHub上去查看源码

使用markdown-it插件添加新语法


VS Code Markdown预览支持CommonMark规格,插件可以通过一个markdown-it插件添加新的Markdown语法。

首先,在你的插件package.json中配置"markdown.markdownItPlugins"

"contributes": {
    "markdown.markdownItPlugins": true
}

然后在插件的主activation函数中,返回一个包含名extendMarkdownIt函数的对象。这个函数接收一个markdown-it实例,然后必须返回出新的markdown-it实例:

import * as vscode from 'vscode'
export function activate(context: vscode.ExtensionContext) {
    return {
        extendMarkdownIt(md: any) {
            return md.use(require('markdown-it-emoji'));
        }
    }
}

若想配置多个markdown-it插件,只需多次链式调用use声明即可。

return md.use(require('markdown-it-emoji')).use(require('markdown-it-hashtag'));

Markdown预览第一次显示时,配置了markdown-it的插件会变成懒加载激活。

markdown-emoji插件展示了如何使用markdown-it添加emoji支持,你可以在GitHub上查看Emoji插件的源码

你可能还想了解:

用脚本添加进阶功能


对于进阶特性,在插件中配置可运行的脚本:

"contributes": {
    "markdown.previewScripts": [
        "./main.js"
    ]
}

配置的脚本是异步加载的,每次内容变动还会重载。

Markdown Preview Mermaid Support插件展示了如何使用脚本添加鱼骨图和流程图预览。在这里查看插件源码。

语言插件

概述

VS Code通过语言插件可以为各式各样的编程语言提供智能的编辑体验。VS Code并不含内置语言支持,不过提供了一整套支持富文本特性的API。 比如,HTML插件是一个可以为VS Code中的HTML文件提供语法高亮的插件。 类似的,当你输入console.时,智能补全会提示log,这是Typescript Language Features插件提供的。

语言特性大致可以分为下面两种:

声明式语言特性


定义在配置文件的语言功能称之为编程式语言特性,比如,htmlcsstypescript-基础支持插件都打包在了VS Code中,所以提供了下列声明式语言特性:

  • 语法高亮
  • 代码片段补全
  • 括号匹配
  • 自动闭合括号
  • 括号识别
  • 启动、关闭注释
  • 自动缩进
  • 代码折叠

我们提供了3个指南供你开发语言插件的声明式特性:

  • 语法高亮指南:VS Code 使用TextMate语法来高亮代码。这个指南将教你用简单的TextMate语法开发一个VS Code插件。
  • 代码片段补全指南: 这个指南教你怎么把代码片段打包进插件中。
  • 语言配置指南:VS Code允许插件为任何编程语言定义 语言配置。这个文件控制着基本的编辑功能,如开闭注释、括号匹配/识别,和(基础)代码折叠。

编程式语言特性


编程式语言特性包括自动补全、错误检查和跳转到定义。这些功能一般通过语言服务器驱动,这个服务器会分析你的项目,然后提供对应的功能。最好的例子就是打包在VS Code中的typescript-language-features插件,它利用TypeScript Language Service提供了诸如下面罗列的编程式语言特性:

下面是编程式语言特性的完整列表。

语言服务器协议(Language Server Protocol)


语言服务器协议将语言服务器(一个静态代码分析工具)和语言客户端(一般就是源代码)之间的通信进行了标准化,这样一来插件开发者就可以只写一次代码分析程序,然后在多个编辑器中重用了。

在编程式语言特性列表中,你可以找到所有VS Code的语言特性,以及它和语言服务器协议规格之间的映射关系。

我们提供了一个非常详尽的指南,里面会告诉你怎么实现一个语言服务器插件:

  • 语言服务器插件指南

特殊功能


多目录工作区支持

当用户打开了一个多目录工作区,你可能需要将你的语言服务器插件做相应的调整。这个主题探讨了几种多目录工作区的语言服务器的实现方法。 (译者注:官方可能尚未完成这个部分的文档)

嵌入式语言

嵌入式语言在web开发中是非常常见的,比如HTML中的CSS/JS,JS/TS中的GraphQL。这个主题探讨了针对嵌入语言实现VS Code语言特性的各种方法。 (译者注:官方可能尚未完成这个部分的文档)

语法高亮

语法高亮决定源代码的颜色和样式,它主要负责关键字(如javascript中的iffor)、字符串、注释、变量名等等语法的着色工作。

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

  • 根据语法将文本解析成符号和作用域
  • 然后根据这份作用域映射应用对应的颜色和样式

本文档只教你第一部分:根据语法将文本解析成符号和作用域,然后使用现成的颜色和样式。自定义样式的部分请参考色彩主题指南

TextMate 语法


VS Code使用TextMate 语法将文本分割成一个个符号。TextMate语法是Oniguruma正则表达式的集合,一般是一份plist或者JSON格式的文件。你可以在这里找到更棒的介绍文档,在里面可以找到你感兴趣的TextMate语法。

符号和作用域

符号是由一门编程语言中最常见的一到几个字符组成的。符号包括运算符(如:+*),变量名(如:myVar),或者字符串(如:"my string")。

每个符号都有其作用域,作用域描述了这个符号的上下文。一个符号可被由符号序列查找到,比如javascript中的+符号有这样的作用域keyword.operator.arithmetic.js

主题会把颜色和样式映射到作用域上,这样一来就实现了语法高亮。TextMate提供了一些主题中常用的作用域,如果你想要尽可能全面地支持语法,最好从现成的主题中入手,避免重新编写主题。

作用域支持嵌套,每个符号都会关联到它的父作用域上。下面的例子使用了作用域检查器,可以清晰地看到javascript函数中的运算符+和它的作用域层级:

父作用域的信息也同样是主题中的一部分。当主题指定了作用域,该作用域下的所有符号都会进行对应的着色,除非主题里面对单个作用域有其特殊配置。

配置基本语法

VS Code支持JSON格式的TextMate语法。你可以在发布内容配置里面的grammers进行配置。

这个配置点可以配置的内容有:语言的id,顶层语法作用域的名称,语法文件的路径。下面是一个abc语言的语法配置文件:

{
    "contributes": {
        "languages": [
            {
                "id": "abc",
                "extensions": [".abc"]
            }
        ],
        "grammars": [
            {
                "language": "abc",
                "scopeName": "source.abc",
                "path": "./syntaxes/abc.tmGrammar.json"
            }
        ]
    }
}

这个语法文件本身包含了一个顶层规则,里面一般分为两个部分,patterns列出了程序(program)和repository的顶层元素。语法中的其他规则需要从repository中使用{ "include": "#id" }引入。

abc语法标记了字母abc作为关键字,可以被括号包起来成为一个表达式。

{
    "scopeName": "source.abc",
    "patterns": [{ "include": "#expression" }],
    "repository": {
        "expression": {
            "patterns": [{ "include": "#letter" }, { "include": "#paren-expression" }]
        },
        "letter": {
            "match": "a|b|c",
            "name": "keyword.letter"
        },
        "paren-expression": {
            "begin": "\\(",
            "end": "\\)",
            "beginCaptures": {
                "0": { "name": "punctuation.paren.open" }
            },
            "endCaptures": {
                "0": { "name": "punctuation.paren.close" }
            },
            "name": "expression.group",
            "patterns": [{ "include": "#expression" }]
        }
    }
}

语法引擎会试着逐步将expression中的规则应用到文本中。比如下面这个简单的程序:

a
(
    b
)
x
(
    (
        c
        xyz
    )
)
(
a

这个例子中的语法产生了下面的作用域列表(从左到右,从最佳匹配到最不匹配)

a               keyword.letter, source.abc
(               punctuation.paren.open, expression.group, source.abc
    b           expression.group, source.abc
)               punctuation.paren.close, expression.group, source.abc
x               source.abc
(               punctuation.paren.open, expression.group, source.abc
    (           punctuation.paren.open, expression.group, expression.group, source.abc
        c       keyword.letter, expression.group, expression.group, source.abc
        xyz     expression.group, expression.group, source.abc
    )           punctuation.paren.close, expression.group, expression.group, source.abc
)               punctuation.paren.close, expression.group, source.abc
(               source.abc
a               keyword.letter, source.abc

注意文本匹配不是单一规则,比如字符串xyz,是包含在当前作用域中的。文件的最后一个括号在expression.group里面,因为不会匹配end规则。

嵌入式语言

如果你的语法中需要在父语言中嵌入其他语言,比如HTML中的CSS,那么你可以使用embeddedLanguages配置,告诉VSCode怎么处理嵌入的语言。然后嵌入语言的括号匹配,注释,和其他基础语言功能都会正常运作。

embeddedLanguages配置将嵌入语言的作用域映射到顶层语言的作用域上。下面里的例子里,meta.embedded.block.javascript作用域中的任何符号都会以javscript处理:

{
    "contributes": {
        "grammars": [
            {
                "path": "./syntaxes/abc.tmLanguage.json",
                "scopeName": "source.abc",
                "embeddedLanguages": {
                    "meta.embedded.block.javascript": "source.js"
                }
            }
        ]
    }
}

现在,如你对应用了meta.embedded.block.javascript的符号进行注释就会有正确的//javascript风格,如果你触发代码片段,也会提示对应的javascript片段。

开发全新的语法插件


使用VS Code的Yeoman模板快速创建一个新的语法插件,运行yo code然后选择New Language

Yeoman通过问问题的方式最后生成新的插件,对于创建语法插件最重要的几点就是:

  • Language Id - 这个语言的id
  • Language Name - 友好的名称
  • Scope names - TextMate根作用域名称

生成器会假设你要同时对新语言定义好语言id和语法。如果你只是根据已有的语言创建新的语法,那么你只要填好目标语言的信息就好,然后一定要删除生成的package.json中的languages部分。

回答了一大堆问题之后,Yeoman会创建一个新的插件,其结构如下:

!> 注意:如果你只是配置一个VS Code中已有语言的语法,记得删掉生成的package.json中的languages配置。

迁移现成的TextMate语法

yo code也快成帮你把已有的TextMate语法转成一个VS Code插件。使用yo code,选择Language extension,当询问是否从已有TextMate文件插件的时候,填入后缀为.tmLanguage.json的TextMate语法文件。

用YAML配置语法

随着语言日益复杂,你可能很快就会难以理解和维护你的json文件。如果你发现自己需要写很多正则表达式,或是需要添加大量解释语法层面的注释,你可能需要考虑使用yaml定义语法文件了。

Yaml语法和json有着同样的结构,但是它的语法更加精简,如多行字符串和注释。

VS Code只能加载json语法,所以yaml格式的语法文件必须最终转换成json文件。js-yaml可以帮你完成这个任务:

# Install js-yaml as a development only dependency in your extension
$ npm install js-yaml --save-dev
# Use the command line tool to convert the yaml grammar to json
$ npx js-yaml syntaxes/abc.tmLanguage.yaml > syntaxes/abc.tmLanguage.json

作用域检查器

VS Code自带的作用域检查器能帮你调试语法文件。它能显示当前位置符号作用域,以及应用在上面的主题规则和元信息。

在命令面板中输入Developer: Inspect TM Scopes或者使用快捷键启动作用域检查器

{
    "key": "cmd+alt+shift+i",
    "command": "editor.action.inspectTMScopes"
}

作用域检查器可以显示以下的信息:

  1. 当前符号
  2. 关于符号的元信息,这些值都是计算后的值。如果你使用了嵌入语言,那么这里最重要的信息就是languagetoken type
  3. 符号使用的主题规则。这里只显示当前应用的规则,而不显示被其他样式覆盖的规则。
  4. 完整的作用域列表,越往上作用域越明确。

语法注入


你可以通过语法注入扩展一个现成的语法文件。语法注入就是常规的TextMate语法,语法注入的应用有:

  • 高亮注释中的关键字,如TODO
  • 对现有语法添加更明确的作用域信息
  • 向Markdown中的代码区块添加语法高亮

创建一个基础语法注入

语法注入也是在package.json中配置的,不过这次不需要配置language,而是配置injectTo指明目需要注入的语言作用域列表。

在这个例子里,我们会新建一个非常简单的注入语法,对javascript注释中的TODO进行高亮。我们在injectTo中用source.js指向目标语言的作用域。

{
    "contributes": {
        "grammars": [
            {
                "path": "./syntaxes/injection.json",
                "scopeName": "todo-comment.injection",
                "injectTo": ["source.js"]
            }
        ]
    }
}

除了顶层的injectionSelector,语法本身就应该是标准的TextMate语法。injectionSelector是一个作用域选择器,它指明了语法注入生效的作用域。在我们的例子里,我们想要在所有//注释中的TODO高亮。使用作用域检查器,我们会发现JavaScript的双斜杠存在作用域comment.line.double-slash,所以我们的注入选择器是L:comment.line.double-slash

{
    "scopeName": "todo-comment.injection",
    "injectionSelector": "L:comment.line.double-slash",
    "patterns": [
        {
            "include": "#todo-keyword"
        }
    ],
    "repository": {
        "todo-keyword": {
            "match": "TODO",
            "name": "keyword.todo"
        }
    }
}

注入选择器中的L:代表注入的语法添加在现有语法规则的左边。也就是说我们注入的语法规则会在任何现有语法规则之前生效。

嵌入语法

语法注入也可以用在嵌入语言中,在他们的父级语法中进行配置。就和普通的语法意义,语法注入也可以使用embeddedLanguages将嵌入语言的作用域映射到顶层的语言作用域上。

比如高亮JS字符串中的sql查询的插件,可以使用embeddedLanguages为字符串中所有匹配meta.embedded.inline.sql的符号应用sql语言的基本功能,比如括号匹配和片段选择。

{
    "contributes": {
        "grammars": [
            {
                "path": "./syntaxes/injection.json",
                "scopeName": "sql-string.injection",
                "injectTo": ["source.js"],
                "embeddedLanguages": {
                    "meta.embedded.inline.sql": "source.sql"
                }
            }
        ]
    }
}

符号类型和嵌入语言

对于嵌入语言中的注入语言还会有个副作用,那就是VS Code把所有字符串(string)中的符号视为字符文本,而且把注释中的所有符号视为符号内容(token content)。 因此诸如括号匹配和自动补全在字符串和注释中是无法使用的,如果嵌入语言刚好出现在字符串或注释中,那么这些功能就无法在嵌入语言中使用。

想要重载这个行为,你需要使用meta.embedded.*作用域重置VS Code标记字符串和注释行为。最佳实践就是始终将嵌入语言放在meta.embedded.*作用域中,确保VS Code能够正确处理嵌入语言。

如果你无法为你的语法添加meta.embedded.*作用域,你可以在语法配置中用tokenTypes,指定作用域到内容模式(content mode)上。 下面的tokenTypes确保my.sql.template.string作用域中的任何内容都应视为代码:

{
    "contributes": {
        "grammars": [
            {
                "path": "./syntaxes/injection.json",
                "scopeName": "sql-string.injection",
                "injectTo": ["source.js"],
                "embeddedLanguages": {
                    "my.sql.template.string": "source.sql"
                },
                "tokenTypes": {
                    "my.sql.template.string": "other"
                }
            }
        ]
    }
}

代码片段

contributes.snippets配置允许你将代码片段打包进VS Code插件中。

创建代码片段主题详细介绍了新建代码片段的全部内容。本篇指南只是告诉你关于打包代码片段的大体思路。比较推荐的做法是:

  • Preferences: Configure User Snippets命令创建和调试代码片段。
  • 如果你觉得满意了,将整个JSON文件复制到插件目录下,起个名字比如说snippets.json文件。
  • 将下列配置添加到你的package.json
{
    "contributes": {
        "snippets": [
            {
                "language": "javascript",
                "path": "./snippets.json"
            }
        ]
    }
}

本篇的源代码在https://github.com/Microsoft/vscode-extension-samples/tree/master/snippet-sample

?> 提示:在package.json中添加如下分类,用户才能轻松找到你的插件。

{
    "categories": ["Snippets"]
}

使用TextMate代码片段


你也可以用yo code将TextMate代码片段(.tmSnippets)直接添加到插件里去。生成器中的可选项New Code Snippets会帮你指向.tmSnippets的目录,它们最后都会一起打包到VS Code 插件里。生成器甚至还支持Sublime代码片段(.sublime-snippets)。

生成器最终输出的文件有两个:一份插件清单package.json,和一份转换为VS Code代码片段的snippets.json

.
├── snippets                    // VS Code integration
│   └── snippets.json           // The JSON file w/ the snippets
└── package.json                // extension's manifest

把生成的代码片段文件夹复制到你的.vscode/extensions下的新文件夹中,然后重启VS Code。

语言配置

通过contributes.languages发布内容配置,你可以配置以下声明式语言特性

  • 启用/关闭注释
  • 定义括号
  • 自动闭合符号
  • 自动环绕符号
  • 代码折叠
  • 单词匹配
  • 缩进规则

语言配置示例中配置JavaScript文件中的编辑功能。本篇指南会详细解释language-configuration.json中的内容:

!> 注意:如果你的语言配置文件以language-configuration.json结尾,那么VS Code会帮你添加代码补全和校验功能。

{
    "comments": {
        "lineComment": "//",
        "blockComment": ["/*", "*/"]
    },
    "brackets": [["{", "}"], ["[", "]"], ["(", ")"]],
    "autoClosingPairs": [
        { "open": "{", "close": "}" },
        { "open": "[", "close": "]" },
        { "open": "(", "close": ")" },
        { "open": "'", "close": "'", "notIn": ["string", "comment"] },
        { "open": "\"", "close": "\"", "notIn": ["string"] },
        { "open": "`", "close": "`", "notIn": ["string", "comment"] },
        { "open": "/**", "close": " */", "notIn": ["string"] }
    ],
    "autoCloseBefore": ";:.,=}])>` \n\t",
    "surroundingPairs": [
        ["{", "}"],
        ["[", "]"],
        ["(", ")"],
        ["'", "'"],
        ["\"", "\""],
        ["`", "`"]
    ],
    "folding": {
        "markers": {
            "start": "^\\s*//\\s*#?region\\b",
            "end": "^\\s*//\\s*#?endregion\\b"
        }
    },
    "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)",
    "indentationRules": {
        "increaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$",
        "decreaseIndentPattern": "^((?!.*?\\/\\*).*\\*/)?\\s*[\\}\\]].*$"
    }
}

启用/关闭注释


VS Code提供了切换注释开关的命令:

  • Toggle Line Comment
  • Toggle Block Comment

分别来配置comments.lineComment控制块注释和comments.blockComment控制行注释。

{
    "comments": {
        "lineComment": "//",
        "blockComment": ["/*", "*/"]
    }
}

定义括号


你在VS Code中将鼠标移动到一个括号边上时,VS Code会自动高亮对应的括号。

{
    "brackets": [["{", "}"], ["[", "]"], ["(", ")"]]
}

另外,当你运行Go to BracketSelect to Bracket时,VS Code会自动使用你的定义找到最近、最匹配的括号。

自动闭合符号


当你输入'时,VS Code会自动帮你补全另一个单引号然后将光标放在引号中间,我们来看看是怎么做的:

{
    "autoClosingPairs": [
        { "open": "{", "close": "}" },
        { "open": "[", "close": "]" },
        { "open": "(", "close": ")" },
        { "open": "'", "close": "'", "notIn": ["string", "comment"] },
        { "open": "\"", "close": "\"", "notIn": ["string"] },
        { "open": "`", "close": "`", "notIn": ["string", "comment"] },
        { "open": "/**", "close": " */", "notIn": ["string"] }
    ]
}

配置notIn键(key)可以在你需要的时候关闭这个功能。比如你在写下面的代码:

// ES6's Template String
`ES6's Template String`;

此时单引号就不会闭合。

用户可以使用editor.autoClosingQuoteseditor.autoClosingBrackets设置自动闭合符号的行为。

在XXX前闭合符号

如果符号的右边有空白,那么VS Code默认会启用符号闭合,所以当你在JSX代码中输入{时,符号并不会闭合:

const Component = () =>
  <div className={>
                  ^ VS Code默认不会闭合此处的括号
  </div>

但是你可以用下面的定义覆盖默认行为:

{
    "autoCloseBefore": ";:.,=}])>` \n\t"
}

现在如果你在>前面输入{,VS Code会自动补全}

自动环绕符号


当你选择了一堆文本然后输入左括号时,VS Code会对选中内容外围加上对应的括号。这个功能叫做自动环绕符号,你可以参考下面的代码指定这项功能:

{
    "surroundingPairs": [
        ["{", "}"],
        ["[", "]"],
        ["(", ")"],
        ["'", "'"],
        ["\"", "\""],
        ["`", "`"]
    ]
}

注意用户可以通过editor.autoSurround设置自动环绕符号的行为。

代码折叠


在VS Code中有三种代码折叠类型:

  • 缩进折叠:这是VS Code中默认的缩进行为,当两行内容有着相同的缩进级别时,你就可以看到折叠标记了。
  • 语言配置折叠:当VS Code发现folding.markers同时定义了startend时,对应区域内就会出现折叠标记。下述配置会对//#region//#endregionJSON区域创建代码折叠标记:
{
    "folding": {
        "markers": {
            "start": "^\\s*//\\s*#?region\\b",
            "end": "^\\s*//\\s*#?endregion\\b"
        }
    }
}
  • 语言服务器折叠:语言服务器获取到textDocument/foldingRange请求中的代码折叠列表数据,VS Code之后根据这份列表创建折叠标记。通过语言服务器协议学习更多关于程序性语言特性

单词匹配


wordPattern定义了程序语言中单词单位。因此当你使用词语相关的命令,如:Move cursor to word start(Ctrl+Left)或者Move cursor to word end(Ctrl+Right)时,编辑器会根据正则寻找单词边界。

{
    "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)"
}

缩进规则


indentationRules定义了编辑器应该如何调整当前行或你粘贴、输入、移动的下一行缩进。

{
    "indentationRules": {
        "increaseIndentPattern": "^((?!\\/\\/).)*(\\{[^}\"'`]*|\\([^)\"'`]*|\\[[^\\]\"'`]*)$",
        "decreaseIndentPattern": "^((?!.*?\\/\\*).*\\*/)?\\s*[\\)\\}\\]].*$"
    }
}

比如,if (true) {匹配increasedIndentPattern,然后如果你在{后面按下Enter后,编辑器会自动缩进一次,你如代码看起来会像这样:

if (true) {
    console.log();

如果没有设置缩进规则,当行尾以开符号结尾时编辑器会左缩进,以闭合符号结尾时右缩进。这里的开闭符号brackets定义。

注意editor.formatOnPaste是由DocumentRangeFormattingEditProvider控制,而不由自动缩进控制。

程序性语言特性

程序性语言特性是由vscode.languages.*API提供的一系列智能编辑功能。在VS Code中有两种实现动态语言特性的途径。我们先以悬停提示为例:

vscode.languages.registerHoverProvider('javascript', {
    provideHover(document, position, token) {
        return {
            contents: ['Hover Content']
        };
    }
});

正如你所见,代码中vscode.languages.registerHoverProviderAPI可以很方便地在JS文件中提供悬停提示的内容。这个插件激活后,只要你悬停到了JS代码上,VS Code就会查询全部对JS注册了的HoverProvider然后在悬浮提示框中显示对应内容。你可以查看下面的语言功能列表,里面包含了VS Code API / LSP实现的语言功能。

那么一种实现就是使用了语言服务器协议的语言服务器。它的实现方式如下:

  • 一个为JS同时提供语言客户端和语言服务器的插件
  • 语言客户端就像普通插件一样,运行于Node.js插件主机环境中。这个插件激活后,会启动另一个进程——语言服务器,然后两者通过语言服务器协议进行通信。
  • 你悬停到JS代码上
  • VS Code通知语言客户端
  • 语言客户端向语言服务器发起请求,索要悬停的返回结果,最后再送回给VS Code
  • VS Code将结果展示在悬浮框中

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

  • 语言服务器可以用任何语言实现
  • 语言服务器可以被多个编辑器重用,提供更加智能的编辑体验

深入指南,请移步至语言服务器插件指南

语言功能列表


VS Code API LSP method
createDiagnosticCollection PublishDiagnostics
registerCompletionItemProvider Completion & Completion Resolve
registerHoverProvider Hover
registerSignatureHelpProvider SignatureHelp
registerDefinitionProvider Definition
registerTypeDefinitionProvider TypeDefinition
registerImplementationProvider Implementation
registerReferenceProvider References
registerDocumentHighlightProvider DocumentHighlight
registerDocumentSymbolProvider DocumentSymbol
registerCodeActionsProvider CodeAction
registerCodeLensProvider CodeLens & CodeLens Resolve
registerDocumentLinkProvider DocumentLink & DocumentLink
registerColorProvider DocumentColor & Color Presentation
registerDocumentFormattingEditProvider Formatting
registerDocumentRangeFormattingEditProvider RangeFormatting
registerOnTypeFormattingEditProvider OnTypeFormatting
registerRenameProvider Rename & Prepare Rename
registerFoldingRangeProvider FoldingRange

提供诊断信息


诊断信息是提示代码问题的一种方式。

语言服务器协议

语言服务器需要向客户端发送textDocument/publishDiagnostics信息,这个信息中包含了诊断信息url的数组。

!> 注意:客户端不会主动向服务端请求信息,需要服务器将诊断信息推送到客户端。

直接实现
let diagnosticCollection: vscode.DiagnosticCollection;
export function activate(ctx: vscode.ExtensionContext): void {
  ...
  ctx.subscriptions.push(getDisposable());
  diagnosticCollection = vscode.languages.createDiagnosticCollection('go');
  ctx.subscriptions.push(diagnosticCollection);
  ...
}
function onChange() {
  let uri = document.uri;
  check(uri.fsPath, goConfig).then(errors => {
    diagnosticCollection.clear();
    let diagnosticMap: Map<string, vscode.Diagnostic[]> = new Map();
    errors.forEach(error => {
      let canonicalFile = vscode.Uri.file(error.file).toString();
      let range = new vscode.Range(error.line-1, error.startColumn, error.line-1, error.endColumn);
      let diagnostics = diagnosticMap.get(canonicalFile);
      if (!diagnostics) { diagnostics = []; }
      diagnostics.push(new vscode.Diagnostic(range, error.msg, error.severity));
      diagnosticMap.set(canonicalFile, diagnostics);
    });
    diagnosticMap.forEach((diags, file) => {
      diagnosticCollection.set(vscode.Uri.parse(file), diags);
    });
  })
}

基础实现

只对打开的编辑器提供诊断,保证至少在每次保存文件时诊断一次。诊断信息最好能随编辑器的文档内容变化触发。

进阶实现

不仅仅为打开的编辑器提供诊断,而是诊断当前打开的文件目录中的所有资源,不论文件是被打开还是关闭。

提供补全建议


代码补全可以给用户提供内容感知建议。

语言服务器协议

在接收响应的initialize方法中,你的语言服务器需要声明它是否能提供补全,以及它是否支持动态计算补全项的completionItem\resolve方法。

{
    ...
    "capabilities" : {
        "completionProvider" : {
            "resolveProvider": "true",
            "triggerCharacters": [ '.' ]
        }
        ...
    }
}
直接实现
class GoCompletionItemProvider implements vscode.CompletionItemProvider {
    public provideCompletionItems(
        document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken):
        Thenable<vscode.CompletionItem[]> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(getDisposable());
    ctx.subscriptions.push(
        vscode.languages.registerCompletionItemProvider(
            GO_MODE, new GoCompletionItemProvider(), '.', '\"'));
    ...
}

基础实现

不支持本功能

进阶实现

当用户挑选补全项时,动态计算补全项的相关信息,这条信息会浮现在补全项旁边。

显示悬浮提示


悬浮信息会展示在鼠标光标的下方,为用户提供符号/对象的相关信息,一般展示关于符号的类型和描述。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "hoverProvider" : "true",
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/hover请求。

直接实现
class GoHoverProvider implements HoverProvider {
    public provideHover(
        document: TextDocument, position: Position, token: CancellationToken):
        Thenable<Hover> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerHoverProvider(
            GO_MODE, new GoHoverProvider()));
    ...
}

基础实现 显示符号的类型和相关文档描述。

进阶实现 对方法名进行着色,就像你的源码一样

函数和方法签名


当用户输入函数和方法时,显示调用该方法的相关信息。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "signatureHelpProvider" : {
            "triggerCharacters": [ '(' ]
        }
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/signatureHelp请求。

直接实现
class GoSignatureHelpProvider implements SignatureHelpProvider {
    public provideSignatureHelp(
        document: TextDocument, position: Position, token: CancellationToken):
        Promise<SignatureHelp> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerSignatureHelpProvider(
            GO_MODE, new GoSignatureHelpProvider(), '(', ','));
    ...
}

基础实现

签名帮助需要包含参数的相关文档。

进阶实现

符号定义


允许用户查看变量/函数/方法的定义。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "definitionProvider" : "true"
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/definition请求。

直接实现
lass GoDefinitionProvider implements vscode.DefinitionProvider {
    public provideDefinition(
        document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken):
        Thenable<vscode.Location> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerDefinitionProvider(
            GO_MODE, new GoDefinitionProvider()));
    ...
}

基础实现

如果符号有多个定义,你可以显示多条定义。

进阶实现

查找符号的全部引用


允许用户在当前编辑器直接查看变量/函数/方法的定义的源代码。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "referencesProvider" : "true"
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/references请求。

直接实现
class GoReferenceProvider implements vscode.ReferenceProvider {
    public provideReferences(
        document: vscode.TextDocument, position: vscode.Position,
        options: { includeDeclaration: boolean }, token: vscode.CancellationToken):
        Thenable<vscode.Location[]> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerReferenceProvider(
            GO_MODE, new GoReferenceProvider()));
    ...
}

基础实现

为所有引用返回引用位置(资源的url和范围)

进阶实现

高亮匹配符号


允许用户在打开的编辑器中查看某个符号的全部匹配项。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "documentHighlightProvider" : "true"
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/documentHighlight请求。

直接实现
class GoDocumentHighlightProvider implements vscode.DocumentHighlightProvider {
    public provideDocumentHighlights(
        document: vscode.TextDocument, position: vscode.Position, token: vscode.CancellationToken):
        vscode.DocumentHighlight[] | Thenable<vscode.DocumentHighlight[]>;
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerDocumentHighlightProvider(
            GO_MODE, new GoDocumentHighlightProvider()));
    ...
}

基础实现

返回引用文档

进阶实现

显示当前文档中的符号定义


允许用户在打开的编辑器中快速跳转到任何符号定义。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "documentSymbolProvider" : "true"
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/documentSymbol请求。

直接实现
class GoDocumentSymbolProvider implements vscode.DocumentSymbolProvider {
    public provideDocumentSymbols(
        document: vscode.TextDocument, token: vscode.CancellationToken):
        Thenable<vscode.SymbolInformation[]> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerDocumentSymbolProvider(
            GO_MODE, new GoDocumentSymbolProvider()));
    ...
}

基础实现

返回文档中的所有符号。将符号分类,如变量、函数、类、方法等。

进阶实现

显示文件夹中的符号定义


允许用户在打开的文件夹(工作区)中快速跳转到任何符号定义。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "workspaceSymbolProvider" : "true"
        ...
    }
}

另外,你的语言服务器还要能够响应workspace/symbol请求。

直接实现
class GoWorkspaceSymbolProvider implements vscode.WorkspaceSymbolProvider {
    public provideWorkspaceSymbols(
        query: string, token: vscode.CancellationToken):
        Thenable<vscode.SymbolInformation[]> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerWorkspaceSymbolProvider(
            new GoWorkspaceSymbolProvider()));
    ...
}

基础实现

返回文件夹中所有匹配的符号。将符号分类,如变量、函数、类、方法等。

进阶实现

处理错误和警告


为用户提供处理错误和警告的办法。如果有更正操作可用,就会在那个错误边上显示一个小灯泡。当用户点击灯泡的时候,会显示出操作列表。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "codeActionProvider" : "true"
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/codeAction请求。

直接实现
class GoCodeActionProvider implements vscode.CodeActionProvider {
    public provideCodeActions(
        document: vscode.TextDocument, range: vscode.Range,
        context: vscode.CodeActionContext, token: vscode.CancellationToken):
        Thenable<vscode.Command[]> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerCodeActionsProvider(
            GO_MODE, new GoCodeActionProvider()));
    ...
}

基础实现

为错误/警告提供更正操作。

进阶实现

提供源码级别的操作,如重构、提取方法等。

CodeLens - 为源代码提供更多操作


为用户弹出一个可以操作、包含上下文信息的分隔弹出框。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能,以及它是否支持将codeLens\resolve方法绑定到CodeLens的命令上。

{
    ...
    "capabilities" : {
        "codeLensProvider" : {
            "resolveProvider": "true"
        }
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/codeLens请求。

直接实现
class GoCodeLensProvider implements vscode.CodeLensProvider {
    public provideCodeLenses(document: TextDocument, token: CancellationToken):
        CodeLens[] | Thenable<CodeLens[]> {
    ...
    }
    public resolveCodeLens?(codeLens: CodeLens, token: CancellationToken):
         CodeLens | Thenable<CodeLens> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerCodeLensProvider(
            GO_MODE, new GoCodeLensProvider()));
    ...
}

基础实现

为文档提供CodeLens结果。

进阶实现

将Codelens结果绑定到响应codeLens/resolve的命令上。

颜色拾取器


允许用户在文件中预览和修改颜色。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "colorProvider" : "true"
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/documentColortextDocument/colorPresentation请求。

直接实现
class GoColorProvider implements vscode.DocumentColorProvider {
    public provideDocumentColors(
        document: vscode.TextDocument, token: vscode.CancellationToken):
        Thenable<vscode.ColorInformation[]> {
    ...
    }
    public provideColorPresentations(
        color: Color, context: { document: TextDocument, range: Range }, token: vscode.CancellationToken):
        Thenable<vscode.ColorPresentation[]> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerColorProvider(
            GO_MODE, new GoColorProvider()));
    ...
}

基础实现

返回文档中的全部颜色引用。在颜色面板岁支持色彩格式(如rgb(…),hsl(…))

进阶实现

格式化代码


提供整个文档的代码格式化支持。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "documentFormattingProvider" : "true"
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/formatting请求。

直接实现
class GoDocumentFormatter implements vscode.DocumentFormattingEditProvider {
    public formatDocument(document: vscode.TextDocument):
        Thenable<vscode.TextEdit[]> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerDocumentFormattingEditProvider(
            GO_MODE, new GoDocumentFormatter()));
    ...
}

基础实现

不提供格式化支持

进阶实现

你应该尽量减少代码格式化的影响。稍有不慎,诊断功能就可能失效。

格式化选中区域


为用户选中区域提供代码格式化支持。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "documentRangeFormattingProvider" : "true"
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/rangeFormatting请求。

直接实现
class GoDocumentRangeFormatter implements vscode.DocumentRangeFormattingEditProvider{
    public provideDocumentRangeFormattingEdits(
        document: vscode.TextDocument, range: vscode.Range,
        options: vscode.FormattingOptions, token: vscode.CancellationToken):
        Thenable<vscode.TextEdit[]>;
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerDocumentRangeFormattingEditProvider(
            GO_MODE, new GoDocumentRangeFormatter()));
    ...
}

基础实现

不提供格式化支持

进阶实现

你应该尽量减少代码格式化的影响。稍有不慎,诊断功能就可能失效。

随用户输入格式化代码


支持用户输入时动态调整文本格式。

!> 注意:用户设置中的editor.formatOnType控制着本功能。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。服务器还得告诉客户端哪些字符需要被格式化,moreTriggerCharacters是可选的。

{
    ...
    "capabilities" : {
        "documentOnTypeFormattingProvider" : {
            "firstTriggerCharacter": "}",
            "moreTriggerCharacter": [";", ","]
        }
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/onTypeFormatting请求。

直接实现
class GoOnTypingFormatter implements vscode.OnTypeFormattingEditProvider{
    public provideOnTypeFormattingEdits(
        document: vscode.TextDocument, position: vscode.Position,
        ch: string, options: vscode.FormattingOptions, token: vscode.CancellationToken):
        Thenable<vscode.TextEdit[]>;
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerOnTypeFormattingEditProvider(
            GO_MODE, new GoOnTypingFormatter()));
    ...
}

基础实现

不提供格式化支持

进阶实现

你应该尽量减少代码格式化的影响。稍有不慎,诊断功能就可能失效。

重命名符号


允许用户重命名符号,并更新对应符号的全部引用。

语言服务器协议

为了响应请求initialize方法,语言服务器需要声明它能提供这项功能。

{
    ...
    "capabilities" : {
        "renameProvider" : "true"
        ...
    }
}

另外,你的语言服务器还要能够响应textDocument/rename请求。

直接实现
class GoRenameProvider implements vscode.RenameProvider {
    public provideRenameEdits(
        document: vscode.TextDocument, position: vscode.Position,
        newName: string, token: vscode.CancellationToken):
        Thenable<vscode.WorkspaceEdit> {
    ...
    }
}
export function activate(ctx: vscode.ExtensionContext): void {
    ...
    ctx.subscriptions.push(
        vscode.languages.registerRenameProvider(
            GO_MODE, new GoRenameProvider()));
    ...
}

基础实现

不提供本功能支持。

进阶实现

返回工作区中全部需要生效的编辑区,比如当一个符号在项目的各个地方都被引用时。

示例:语言服务器

就如你在程序性语言特性章节所见,实现语言特性的直接方式是使用languages.*API。但是语言服务器不同,它是另一种语言插件的实现方式。

本章将:

为什么使用语言服务器?


语言服务器是一种可以提升语言编辑体验的特殊VS Code插件。有了语言服务器,你可以实现如自动补全、错误检查(诊断)、转跳到定义等等其他VS Code语言特性。

但是在VS Code中实现语言功能会面临三个问题:

第一,语言服务器一般是用他们自己原生的语言实现的,那么如何与VS Code中的Node.js运行时整合起来就是一个问题。

其二,语言服务器一般都是高消耗的。比如检查文件,语言服务器需要解析大量的文件,构建起抽象语法树然后进行静态分析。这些操作会吃掉很多CPU和内存,但是与此同时VS Code的性能不能受到任何影响。

第三,通常为多个编辑器开发不同的语言插件需要花费大量精力。对于语言插件开发者来说,他们需要根据不同编辑器各自的API来实现插件。而从编辑器的角度来讲,他们也不能指望语言工具API统一。最终导致了为N种编辑器实现M种语言需要花费N*M的工作和精力。

为了解决这些问题,微软提供了语言服务器协议(Language Server Protocol)意图为语言插件和编辑器提供社区规范。这样一来,语言服务器就可以用任何一种语言来实现,用协议通讯也避免了插件在主进程中运行的高开销。而且任何LSP兼容的语言插件,都能和LSP兼容的代码编辑器整合起来,LSP是语言插件开发者和第三方编辑器的共赢方案。

在本章,我们将:

  • 根据Node SDK,学习如何在VS Code中新建一个语言服务器插件
  • 学习如何运行、调试、记录日志和测试语言服务器插件
  • 为你提供更多进阶的语言服务器

?> 译者注:本文及其他章节所涉及的LSP全为Language Server Protocol的缩写。语言服务器协议是VS Code为了调试、分析语言的自带的中间层协议。众所周知,VS Code本身只是一个编辑器,它不含任何编程语言的功能和运行时(javascript和typescript除外),而是将语言的各种特性交给了插件创作者自由实现。

实现你自己的语言服务器


在VS Code中,一个语言服务器有两个部分:

  • 语言客户端:一个由Javascript/Typescript组成的普通插件,这个插件能使用所有的VS Code 命名空间API。
  • 语言服务器:运行在单独进程中的语言分析工具。

语言服务器运行在单独的进程有两个好处:

  • 只要能通过LSP通信,语言分析工具可以用任何语言实现。
  • 语言分析工具一般非常消耗CPU和内存,在单独的进程中运行能避免大性能开销

下面是一个运行了2个语言服务器插件的示意图。HTML语言客户端和PHP语言客户端是常见的VS Code插件。两个客户端都用LSP与各自对应的语言服务器进行通信——即使PHP语言服务器是用PHP写的,但是仍然能通过LSP与PHP语言客户端建立起通信。

本篇将指引你学习如何用我们的Node SDK构建一个语言客户端/服务器。剩下的内容都建立在你已经了解VS Code插件开发的基础之上。

示例:一个简单的纯文本语言服务器


让我们首先实现一个简单的语言服务器插件吧,这个插件的功能是自动补全、诊断纯文本文件。我们会同时学习客户端/服务端的配置。 如果你想直接上手代码:

复制Microsoft/vscode-extension-samples然后打开示例:

> cd lsp-sample
> npm install
> npm run compile
> code .

安装完所有依赖然后打开lsp-sample工作,里面包含客户端和服务器的代码。下面是一个整体的lsp-sample目录结构:

.
├── client // 语言客户端
│   ├── src
│   │   ├── test // 语言客户端 / 服务器 的端到端测试
│   │   └── extension.ts // 语言客户端入口
├── package.json // 插件配置清单
└── server // 语言服务器
    └── src
        └── server.ts // 语言服务器入口

什么是’Language Client’


我们先看看/package.json,这个文件描述了语言客户端的能力。里面有3个有趣的部分:

首先看看activationEvents:

"activationEvents": [
    "onLanguage:plaintext"
]

这个部分告诉VS Code只要打开纯文本文件之后就立刻激活插件(例如:打开一个.txt文件)

下一步看看configuration部分:

"configuration": {
    "type": "object",
    "title": "Example configuration",
    "properties": {
        "languageServerExample.maxNumberOfProblems": {
            "scope": "resource",
            "type": "number",
            "default": 100,
            "description": "Controls the maximum number of problems produced by the server."
        }
    }
}

这个部分配置了用户可以自定义的configuration,用户通过这个配置可以在设置中对你的插件做一些修改。这并不是本节重点,稍后示例将通过代码呈现——插件如何在设置变动后将修改后的配置应用到我们的语言服务器上。

真正的语言客户端代码和对应的package.json/client文件夹中。package.json最有趣的部分是vscode插件主机API和vscode-languageclient这两个依赖库。

"dependencies": {
    "vscode": "^1.1.18",
    "vscode-languageclient": "^4.1.4"
}

正如上面所说,客户端实现就是一个普通的VS Code插件,它有使用全部VS Code API的能力。

下面是extension.ts文件的对应内容,也是lsp-sample插件的入口:

import * as path from 'path';
import { workspace, ExtensionContext } from 'vscode';
import {
    LanguageClient,
    LanguageClientOptions,
    ServerOptions,
    TransportKind
} from 'vscode-languageclient';
let client: LanguageClient;
export function activate(context: ExtensionContext) {
    // 服务器由node实现
    let serverModule = context.asAbsolutePath(
        path.join('server', 'out', 'server.js')
    );
    // 为服务器提供debug选项
    // --inspect=6009: 运行在Node's Inspector mode,这样VS Code就能调试服务器了
    let debugOptions = { execArgv: ['--nolazy', '--inspect=6009'] };
    // 如果插件运行在调试模式那么就会使用debug server options
    // 不然就使用run options
    let serverOptions: ServerOptions = {
        run: { module: serverModule, transport: TransportKind.ipc },
        debug: {
            module: serverModule,
            transport: TransportKind.ipc,
            options: debugOptions
        }
    };
    // 控制语言客户端的选项
    let clientOptions: LanguageClientOptions = {
        // 注册纯文本服务器
        documentSelector: [{ scheme: 'file', language: 'plaintext' }],
        synchronize: {
            // 当文件变动为'.clientrc'中那样时,统治服务器
            fileEvents: workspace.createFileSystemWatcher('**/.clientrc')
        }
    };
    // 创建语言客户端并启动
    client = new LanguageClient(
        'languageServerExample',
        'Language Server Example',
        serverOptions,
        clientOptions
    );
    // 启动客户端,这也同时启动了服务器
    client.start();
}
export function deactivate(): Thenable<void> {
    if (!client) {
        return undefined;
    }
    return client.stop();
}

什么是’Language Server’


?> 小提示:本节从Github仓库中克隆下来的’server’代码是已经完成的版本,如果你需要跟随本节的步骤循序渐进,你可以新建一个server.ts或者修改克隆的代码。

在这个例子中,服务器是Typescript实现的,由Node.js运行。因为VS Code自带Node.js运行时,所以你无需安装其他依赖,除非你对运行时有特别要求。

这个语言服务器的源码在/server中。比较重要的pacakge.json部分是:

"dependencies": {
    "vscode-languageserver": "^4.1.3"
}

这行依赖会下载vscode-languageserver库。

下面是一个服务器的实现,提供了简单的纯文本管理——VS Code会向服务器发送一个文件的全部内容。

import {
    createConnection,
    TextDocuments,
    TextDocument,
    Diagnostic,
    DiagnosticSeverity,
    ProposedFeatures,
    InitializeParams,
    DidChangeConfigurationNotification,
    CompletionItem,
    CompletionItemKind,
    TextDocumentPositionParams
} from 'vscode-languageserver';
// 创建一个服务器连接。使用Node的IPC作为传输方式。
// 也包含所有的预览、建议等LSP特性
let connection = createConnection(ProposedFeatures.all);
// 创建一个简单的文本管理器。
// 文本管理器只支持全文本同步。
let documents: TextDocuments = new TextDocuments();
let hasConfigurationCapability: boolean = false;
let hasWorkspaceFolderCapability: boolean = false;
let hasDiagnosticRelatedInformationCapability: boolean = false;
connection.onInitialize((params: InitializeParams) => {
    let capabilities = params.capabilities;
    // 客户端是否支持`workspace/configuration`请求?
    // 如果不是的话,降级到使用全局设置
    hasConfigurationCapability =
        capabilities.workspace && !!capabilities.workspace.configuration;
    hasWorkspaceFolderCapability =
        capabilities.workspace && !!capabilities.workspace.workspaceFolders;
    hasDiagnosticRelatedInformationCapability =
        capabilities.textDocument &&
        capabilities.textDocument.publishDiagnostics &&
        capabilities.textDocument.publishDiagnostics.relatedInformation;
    return {
        capabilities: {
            textDocumentSync: documents.syncKind,
            // 告诉客户端,服务器支持代码补全
            completionProvider: {
                resolveProvider: true
        }
    }
    };
});
connection.onInitialized(() => {
    if (hasConfigurationCapability) {
        // 为所有配置Register for all configuration changes.
        connection.client.register(
            DidChangeConfigurationNotification.type,
            undefined
        );
    }
    if (hasWorkspaceFolderCapability) {
        connection.workspace.onDidChangeWorkspaceFolders(_event => {
            connection.console.log('Workspace folder change event received.');
        });
    }
});
// 配置示例
interface ExampleSettings {
    maxNumberOfProblems: number;
}
// 当客户端不支持`workspace/configuration`请求时,使用global settings
// 请注意,在这个例子中服务器使用的客户端并不是问题所在,而是这种情况还可能发生在其他客户端身上。
const defaultSettings: ExampleSettings = { maxNumberOfProblems: 1000 };
let globalSettings: ExampleSettings = defaultSettings;
// 对所有打开的文档配置进行缓存
let documentSettings: Map<string, Thenable<ExampleSettings>> = new Map();
connection.onDidChangeConfiguration(change => {
    if (hasConfigurationCapability) {
        // 重置所有已缓存的文档配置
        documentSettings.clear();
    } else {
        globalSettings = <ExampleSettings>(
            (change.settings.languageServerExample || defaultSettings)
        );
    }
    // 重新验证所有打开的文本文档
    documents.all().forEach(validateTextDocument);
});
function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
    if (!hasConfigurationCapability) {
        return Promise.resolve(globalSettings);
    }
    let result = documentSettings.get(resource);
    if (!result) {
        result = connection.workspace.getConfiguration({
            scopeUri: resource,
            section: 'languageServerExample'
        });
        documentSettings.set(resource, result);
    }
    return result;
}
// 只对打开的文档保留设置
documents.onDidClose(e => {
    documentSettings.delete(e.document.uri);
});
// 文档的文本内容发生了改变。
// 这个事件在文档第一次打开或者内容变动时才会触发。
documents.onDidChangeContent(change => {
    validateTextDocument(change.document);
});
async function validateTextDocument(textDocument: TextDocument): Promise<void> {
    // 在这个简单的示例中,每次校验运行时我们都获取一次配置
    let settings = await getDocumentSettings(textDocument.uri);
    // 校验器如果检测到连续超过2个以上的大写字母则会报错
    let text = textDocument.getText();
    let pattern = /\b[A-Z]{2,}\b/g;
    let m: RegExpExecArray;
    let problems = 0;
    let diagnostics: Diagnostic[] = [];
    while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
        problems++;
        let diagnosic: Diagnostic = {
            severity: DiagnosticSeverity.Warning,
            range: {
                start: textDocument.positionAt(m.index),
                end: textDocument.positionAt(m.index + m[0].length)
            },
            message: `${m[0]} is all uppercase.`,
            source: 'ex'
        };
        if (hasDiagnosticRelatedInformationCapability) {
            diagnosic.relatedInformation = [
                {
                    location: {
                        uri: textDocument.uri,
                        range: Object.assign({}, diagnosic.range)
                    },
                    message: 'Spelling matters'
                },
                {
                    location: {
                        uri: textDocument.uri,
                        range: Object.assign({}, diagnosic.range)
                    },
                    message: 'Particularly for names'
                }
            ];
        }
        diagnostics.push(diagnosic);
    }
    // 将错误处理结果发送给VS Code
    connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
connection.onDidChangeWatchedFiles(_change => {
    // 监测VS Code中的文件变动
    connection.console.log('We received an file change event');
});
// 这个处理函数提供了初始补全项列表
connection.onCompletion(
    (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
    // 传入的变量包含了文本请求代码补全的位置。
    // 如果我们忽略了这个信息,那就只能提供同样的代码补全项了。
    return [
        {
            label: 'TypeScript',
            kind: CompletionItemKind.Text,
            data: 1
        },
        {
            label: 'JavaScript',
            kind: CompletionItemKind.Text,
            data: 2
        }
        ];
    }
);
// 这个函数为补全列表的选中项提供了更多信息
connection.onCompletionResolve(
    (item: CompletionItem): CompletionItem => {
        if (item.data === 1) {
            item.detail = 'TypeScript details';
            item.documentation = 'TypeScript documentation';
        } else if (item.data === 2) {
            item.detail = 'JavaScript details';
            item.documentation = 'JavaScript documentation';
        }
        return item;
    }
);
/*
connection.onDidOpenTextDocument((params) => {
    // A text document got opened in VSCode.
    // params.uri uniquely identifies the document. For documents store on disk this is a file URI.
    // params.text the initial full content of the document.
    connection.console.log(`${params.textDocument.uri} opened.`);
});
connection.onDidChangeTextDocument((params) => {
    // The content of a text document did change in VSCode.
    // params.uri uniquely identifies the document.
    // params.contentChanges describe the content changes to the document.
    connection.console.log(`${params.textDocument.uri} changed: ${JSON.stringify(params.contentChanges)}`);
});
connection.onDidCloseTextDocument((params) => {
    // A text document got closed in VSCode.
    // params.uri uniquely identifies the document.
    connection.console.log(`${params.textDocument.uri} closed.`);
});
*/
// 让文档管理器监听文档的打开,变动和关闭事件。
documents.listen(connection);
// 连接后启动监听
connection.listen();

添加一个简单的语法校验器


为了给服务器添加文本校验,我们给text document manager添加一个listener然后在文本变动时调用,接下来就交给服务器去判断调用校验器的最佳时机了。在我们的示例中,服务器的功能是校验纯文本然后给所有大写单词进行标记。对应的代码片段:

// 事件在文档第一次打开,或者内容变动时触发。
documents.onDidChangeContent(async (change) => {
    // 在这个简单的示例中,每次校验运行时我们都获取一次配置
    let settings = await getDocumentSettings(textDocument.uri);
    // 校验器如果检测到连续超过2个以上的大写字母则会报错
    let text = textDocument.getText();
    let pattern = /\b[A-Z]{2,}\b/g;
    let m: RegExpExecArray;
    let problems = 0;
    let diagnostics: Diagnostic[] = [];
    while ((m = pattern.exec(text))) {
        problems++;
        let diagnosic: Diagnostic = {
            severity: DiagnosticSeverity.Warning,
            range: {
                start: textDocument.positionAt(m.index),
                end: textDocument.positionAt(m.index + m[0].length)
            },
            message: `${m[0]} is all uppercase.`,
            source: 'ex'
        };
        if (hasDiagnosticRelatedInformationCapability) {
            diagnosic.relatedInformation = [
                {
                    location: {
                        uri: textDocument.uri,
                        range: Object.assign({}, diagnosic.range)
                    },
                    message: 'Spelling matters'
                },
                {
                    location: {
                        uri: textDocument.uri,
                        range: Object.assign({}, diagnosic.range)
                    },
                    message: 'Particularly for names'
                }
            ];
        }
        diagnostics.push(diagnosic);
    }
    // 将错误处理结果发送给VS Code
    connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

诊断提示和小技巧


  • 如果出错的开始点和结束点在同一个位置,VS Code会在那个单词的位置上打上波浪线
  • 如果你想要把波浪线加到行未为止,就把end position设置为Number.MAX_VALUE

运行语言服务器步骤:

  1. 通过快捷键(Ctrl+Shift+B)启动build任务。这个任务会把客户端和服务器端都编译掉。
  2. 打开调试侧边栏,选择启动客户端加载配置,然后按开始调试按钮启动扩展开发主机
  3. 在根目录下新建一个’test.txt’文件,然后粘贴下述内容:
TypeScript lets you write JavaScript the way you really want to.
TypeScript is a typed superset of JavaScript that compiles to plain JavaScript.
ANY browser. ANY host. ANY OS. Open Source.

扩展开发主机实例看起来像是这样:

调试客户端和服务端


调试客户端代码就像调试普通插件一样简单。在代码中打上断点,然后按F5启动插件调试。

因为服务器是由LanguageClient启动的,我们需要附加一个调试器给运行中的服务器。为了做到这一点,切换到调试侧边栏,选择加载配置Attach to Server然后按F5启动调试(要保证server已经启动哦,也就是上面一步),看起来会像这样:

为语言服务器加上日志


如果你是用vscode-languageclient实现的客户端,你可以配置[langId].trace.server指示客户端在output(输出)面板中显示通信日志。

对于Isp-sample你能在"languageServerExample.trace.server": "verbose"进行配置。现在看看”Language Server Example”频道,你应该能看到这些日志:

因为语言服务器通信会非常啰嗦(5s的正常使用会产生5000行日志),因此我们提供了一个可视化和可筛选的日志工具。你可以先从频道中保存所有的日志,然后在语言服务器协议检查器中加载。

在服务器中设置Configuration


当我们写插件的客户端部分的时候,我们已经定义了一个控制最大问题报告数的配置。所以我们也可以在服务器中写一段读取客户端配置的代码:

function getDocumentSettings(resource: string): Thenable<ExampleSettings> {
    if (!hasConfigurationCapability) {
        return Promise.resolve(globalSettings);
    }
    let result = documentSettings.get(resource);
    if (!result) {
        result = connection.workspace.getConfiguration({
            scopeUri: resource,
            section: 'languageServerExample'
        });
        documentSettings.set(resource, result);
    }
    return result;
}

现在唯一要做的事情就是在服务器端中监听用户修改的设置变动,然后重新验证已经打开的文本文件。为了重用文本变动事件的处理函数,我们把代码提取到validateTextDocument函数中,然后新建一个maxNumberOfProblems变量:

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
    // 在这个简单的示例中,每次校验运行时我们都获取一次配置
    let settings = await getDocumentSettings(textDocument.uri);
    // 校验器如果检测到连续超过2个以上的大写字母则会报错
    let text = textDocument.getText();
    let pattern = /\b[A-Z]{2,}\b/g;
    let m: RegExpExecArray;
    let problems = 0;
    let diagnostics: Diagnostic[] = [];
    while ((m = pattern.exec(text)) && problems < settings.maxNumberOfProblems) {
        problems++;
        let diagnosic: Diagnostic = {
            severity: DiagnosticSeverity.Warning,
            range: {
                start: textDocument.positionAt(m.index),
                end: textDocument.positionAt(m.index + m[0].length)
            },
            message: `${m[0]} is all uppercase.`,
            source: 'ex'
        };
        if (hasDiagnosticRelatedInformationCapability) {
            diagnosic.relatedInformation = [
                {
                    location: {
                        uri: textDocument.uri,
                        range: Object.assign({}, diagnosic.range)
                    },
                    message: 'Spelling matters'
                },
                {
                    location: {
                        uri: textDocument.uri,
                        range: Object.assign({}, diagnosic.range)
                    },
                    message: 'Particularly for names'
                }
            ];
        }
        diagnostics.push(diagnosic);
    }
    // 将错误处理结果发送给VS Code
    connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}

添加一个通知处理函数监听配置文件变动。

connection.onDidChangeConfiguration(change => {
    if (hasConfigurationCapability) {
        // Reset all cached document settings
        documentSettings.clear();
    } else {
        globalSettings = <ExampleSettings>(
            (change.settings.languageServerExample || defaultSettings)
        );
    }
    // 重新验证所有打开的文本文档
    documents.all().forEach(validateTextDocument);
});

再次启动客户端,然后把设置中的maximum report改为1,就能看到:

添加其他语言特性


第一个有趣的东西是,语言服务器通常会实现成文档校验器,从这个点来说,即使一个linter也算一个语言服务器,所以VS Code中的linter通常都是作为语言服务器实现的(参照eslintjslint)。但是语言服务器还能做得更多,他们能提供代码不全,查找所有匹配项或者转跳到定义。下面的代码展示了为服务器添加代码补全的功能,它提供了2个建议单词”TypeScript”和”JavaScript”。

// 这个处理函数提供了初始补全项列表
connection.onCompletion(
    (_textDocumentPosition: TextDocumentPositionParams): CompletionItem[] => {
        // 传入的变量包含了文本请求代码补全的位置。
        // 如果我们忽略了这个信息,那就只能提供同样的代码补全项了。
        return [
            {
                label: 'TypeScript',
                kind: CompletionItemKind.Text,
                data: 1
            },
            {
                label: 'JavaScript',
                kind: CompletionItemKind.Text,
                data: 2
            }
        ];
    }
);
// 这个函数为补全列表的选中项提供了更多信息
connection.onCompletionResolve(
    (item: CompletionItem): CompletionItem => {
        if (item.data === 1) {
            (item.detail = 'TypeScript details'),
                (item.documentation = 'TypeScript documentation');
        } else if (item.data === 2) {
            (item.detail = 'JavaScript details'),
                (item.documentation = 'JavaScript documentation');
        }
        return item;
    }
);

data字段用于鉴别处理函数中传入的补全项。这个属性对协议来说是透明的,因为底层协议信息传输是基于JSON的,因此data字段只能保留从JSON序列化而来的数据。

那么现在只缺告诉VS Code服务器能提供代码补全请求。为了做到点,将对应标记添加到初始化函数中:

connection.onInitialize((params): InitializeResult => {
    ...
    return {
        capabilities: {
            ...
            // 告诉客户端,服务器支持代码补全
            completionProvider: {
                resolveProvider: true
            }
        }
    };
});

下面的截屏显示了运行在纯文本文件中的补全代码:

测试语言服务器


为了创建一个高质量的语言服务器,我们需要构建一个能覆盖到它所有功能点的测试套件。有两种常见的测试服务器的方式:

  • 单元测试:如果你想测试特定的功能点,这是一个非常有用的方式,模拟数据然后发送进去。VC Code的HTML/CSS/JSON语言服务器就采用了这种测试方式。LSP的npm模块包也是用这种方式。在这里查看更多使用npm协议模块的单元测试。
  • 端到端测试:就像VS Code 插件测试一样,这个方式的好处是通过运行VS Code实例,打开文件,激活语言服务器/客户端然后执行VS Code命令来测试的,如果你配置了文件、设置和依赖(如node_modules)以及难以模拟数据的时候,你应该优先考虑这种模式,流行的Python插件就采用了这种测试方式。

你可以用任何你喜欢的测试框架做单元测试。这里我们只介绍如何对语言服务器插件进行端到端测试。

打开.vscode/launch.json,你能找到E2E测试目标:

{
    "name": "Language Server E2E Test",
    "type": "extensionHost",
    "request": "launch",
    "runtimeExecutable": "${execPath}",
    "args": [
        "--extensionDevelopmentPath=${workspaceRoot}",
        "--extensionTestsPath=${workspaceRoot}/client/out/test",
        "${workspaceRoot}/client/testFixture"
    ],
    "stopOnEntry": false,
    "sourceMaps": true,
    "outFiles": ["${workspaceRoot}/client/out/test/**/*.js"]
}

如果你运行了这个测试目标,它会打开一个VS Code实例和一个叫做client/testFixtur的激活工作区。VS Code然后会执行所有client/src/test中的测试。一点调试的小提示,你可以在client/src/test的Typescript文件中添加断点。

我们再来看看completion.test.ts文件:

import * as vscode from 'vscode';
import * as assert from 'assert';
import { getDocUri, activate } from './helper';
describe('Should do completion', () => {
    const docUri = getDocUri('completion.txt');
    it('Completes JS/TS in txt file', async () => {
        await testCompletion(docUri, new vscode.Position(0, 0), {
            items: [
                { label: 'JavaScript', kind: vscode.CompletionItemKind.Text },
                { label: 'TypeScript', kind: vscode.CompletionItemKind.Text }
            ]
        });
    });
});
async function testCompletion(
    docUri: vscode.Uri,
    position: vscode.Position,
    expectedCompletionList: vscode.CompletionList
) {
    await activate(docUri);
    // 执行 `vscode.executeCompletionItemProvider` 命令,模拟激活代码补全功能
    const actualCompletionList = (await vscode.commands.executeCommand(
        'vscode.executeCompletionItemProvider',
        docUri,
        position
    )) as vscode.CompletionList;
    assert.equal(actualCompletionList.items.length, expectedCompletionList.items.length);
    expectedCompletionList.items.forEach((expectedItem, i) => {
        const actualItem = actualCompletionList.items[i];
        assert.equal(actualItem.label, expectedItem.label);
        assert.equal(actualItem.kind, expectedItem.kind);
    });
}

在这个测试中,我们:

  • 激活了插件
  • 带上了一个URI和位置模拟信息,然后运行了vscode.executeCompletionItemProvider去触发补全
  • 断言返回的补全项是不是达到了我们的预期

我们再深入一点看看activate(docURI)函数。它被定义在client/src/test/helper.ts中:

import * as vscode from 'vscode';
import * as path from 'path';
export let doc: vscode.TextDocument;
export let editor: vscode.TextEditor;
export let documentEol: string;
export let platformEol: string;
/**
 * 激活 vscode.lsp-sample 插件
 */
export async function activate(docUri: vscode.Uri) {
    // extensionId来自于package.json中的`publisher.name`
    const ext = vscode.extensions.getExtension('vscode.lsp-sample');
    await ext.activate();
    try {
        doc = await vscode.workspace.openTextDocument(docUri);
        editor = await vscode.window.showTextDocument(doc);
        await sleep(2000); // 等待服务器激活
    } catch (e) {
        console.error(e);
    }
}
async function sleep(ms: number) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

在激活部分,我们:

  • publisher.name extensionIdpackage.json中获取到了插件
  • 打开特定的文档,然后显示在文本编辑区
  • 休眠2秒,确保启动了语言服务器

准备好之后,我们可以运行对应语言特性的VS Code命令,然后对结果进行断言测试。 这还有一个关于诊断特性的测试实现,如果你感兴趣,可以查看这个文件client/src/test/diagnostics.test.ts

进阶主题


到目前为止,本篇教程提供了:

  • 一个简短的语言服务器语言服务器协议概览
  • VS Code中的语言服务器插件架构
  • 实现了一个Isp-sample插件,和如何开发、调试、检查和测试语言服务器

更多语言服务器特性

除了代码补全之外,VS Code还支持下列特性:

  • 文档高亮:高亮文本中的符号
  • 悬停:为选中的文本符号提供悬停信息
  • Signature Help:为选中的文本提供提供Signature Help
  • 转跳到定义:为选中的文本符号提供定义转跳
  • 转跳到类型定义:为选中的文本符号提供类型/接口定义转跳
  • 转跳到实现:为选中的文本符号提供实现转跳
  • 引用查找:从整个项目中查找选中文本符号的引用
  • 列出文件符号:列出文本文件中的全部符号
  • 列出工作区符号:列出整个项目中的符号
  • 执行代码:在给定文件和范围的条件下运行命令(通常如:美化、重构)
  • CodeLens: 为给定文件计算 CodeLens 统计数据
  • 文件格式化:包括整个文件的格式化,部分文本格式化和根据类型格式化
  • 重命名:重命名整个项目内的某些符号
  • 文件链接:计算和解析文件中的链接
  • 文件色彩:计算和解析文件中的色彩,并提供编辑器内的取色器

程序性语言特性章节详细介绍了上述的语言特性,并且告诉我们如何通过下述(两者之一)去实现它们:

  • 语言服务器协议
  • 直接使用VS Code的可拓展性API

增量文本同步更新


vscode-languageserver模块中,我们做了一个简单的text document manager同步VS Code和语言服务器。

但是这种方式有两个缺点:

  • 文件变动时,会重复地发送整个文本数据,这个传递的数据量相当可观。
  • 现有的库通常都支持增量文本更新,不可避免地,我们会进行不必要的转换和创建抽象语法树。

LSP因此直接提供了增量文本更新的API。

现在我们要通过增加3个通知函数实现我们的增量文本更新:

  • onDidOpenTextDocument:当文件打开后调用
  • onDidChangeTextDocument:当文本变动后调用
  • onDidCloseTextDocument:当文件关闭后调用

下面的代码片段展示了怎么在通信中挂上这些通知函数钩子,在初始化时因如何返回函数:

connection.onInitialize((params): InitializeResult => {
    ...
    return {
        capabilities: {
            // 启用文档增量更新同步
            textDocumentSync: TextDocumentSyncKind.Incremental,
            ...
        }
    };
});
connection.onDidOpenTextDocument((params) => {
    // 当文档打开后触发,params.uri提供了文档的唯一地址。如果文档储存在硬盘上,那么就会是一个file类型的URI
    // params.text——提供了文档一开始的内容
});
connection.onDidChangeTextDocument((params) => {
    // 文档的文本内容发生了改变时触发。
    // params.uri提供了文档的唯一地址。
    // params.contentChanges 包含文档的变动内容
});
connection.onDidCloseTextDocument((params) => {
    // 文档关闭后触发。
    // params.uri提供了文档的唯一地址。
});

直接用VS Code API实现语言特性

语言服务器有这么多好处,只是用来提供VS Code编辑扩展能力就显得有些大材小用了。下面的例子里,我们使用vscode.languages.register[LANGUAGE_FEATURE]Provider选项为某类文件提供一些简单的语言服务器特性。

completions-sample是一个使用vscode.languages.registerCompletionItemProvider为纯文本添加代码片段的例子。

更多例子请参阅https://github.com/Microsoft/vscode-extension-samples

语言服务器的容错解析器

大多数时候,编辑器中的代码都是不完整的,甚至语法都是错的,但是开发人员肯定希望自动补全等语言功能保持正常工作。因此,容错解析器就显得十分必要:解析器仍能从不完整的代码中创建有意义的AST,然后语言服务器根据这份AST提供服务。

我们之前在VS Code中做过PHP的支持,我们意识到PHP官方解析器并没有自带容错,而且也不能直接在语言服务器中直接重用。所以我们一起努力做了 Microsoft/tolerant-php-parser,并留下了详细的笔记,或许能帮上需要容错解析器的语言服务器作者。

FAQ

  • 问:当我试着向debug添加服务器的时候,我得到了”cannot connect to runtime process (timeout after 5000ms)”的信息?

    答:如果服务器没有运行你还强行添加debbuger的时候,会出现这个超时问题,你也可能需要关闭服务器中的断点。

  • 问:虽然我看完了LSP Specification,但是我还有很多问题解决不了,我可以在哪获得帮助?

    答:可以在https://github.com/Microsoft/language-server-protocol中开issue。

进阶主题

插件主机

在插件诊断中学习了插件会将activedeactive声明周期函数暴露给VS Code,在本节我们来看看插件主机/插件宿主(Extension Host)是怎么管理所有运行中的插件的。

插件主机是VS Code中负责加载和运行插件的Node.js进程,虽然你在写插件时不必关心这件事,但是掌握插件主机的运行原理对你创作插件还是非常有用的。

稳定性和性能


VS Code致力于为用户提供一个稳定且高性能的编辑环境,因此出错的插件不应该影响到用户的体验。所以插件主机可以预防这些事情:

  • 启动性能影响
  • 阻塞的UI操作
  • 修改UI

另外,VS Code提供的激活事件机制也让插件只在用到时才懒加载它们,比如,Markdown插件应该只在用户打开了Markdown文件时才启动,因此避免了不必要CPU和内存消耗。

支持远程开发

VS Code 远程开发允许你无缝在远程机器上开发代码。有了这项支持后,你就可以完全使用VS Code本地插件和你熟悉的方式远程工作了。

本节会介绍远程开发相关的知识,VS Code远程开发 插件架构,在远程目录测试插件,和远程插件不能正常工作的一些建议。大部分插件不需要改动就能适应远程开发环境,其他的插件也只要稍微改动一点就能适配远程开发了。

架构和插件类型


为了使远程开发尽力透明化,便于理解,我们可以将插件分为两类:

  • UI 插件: 这些插件可以配置VS Code的用户界面,而且只运行在用户的本地机器上。UI插件不能直接访问工作区的文件,或者在工作区的机器上运行脚本/工具。这类插件如:主题、代码片段、语言语法、快捷键映射。
  • 工作区插件: 这类插件运行在工作区所在机器上。当运行在本地时,工作区插件运行在本地机器上;在远程项目中,工作区插件运行在远程机器上。工作区插件可以访问工作区的文件并提供富文本支持和多语言服务器,调试和其他复杂的操作(也包含被脚本/工具调起的文件)。不过工作区插件在自定义UI上稍有限制,你可以配置的UI组件有资源管理器,视图容器等其他UI组件。

当一个用户安装了一个插件,VS Code会基于插件类型自动将插件安装到正确的环境中去:UI 插件运行在VS Code的本地插件主机中,工作区插件则运行在一个非常小的VS Code 服务器远程插件主机中。当你打开一个Windows Subsystem for Linux(WSL)、容器、或者远程SSH主机时这个服务会自动安装(更新)。VS Code还会自动管理这个服务的启停,所以用户根本意识不到它的存在。

VS Code API 会自动运行在正确的机器上(不管是本地还是远程)。但是如果你的插件使用的api不是VS Code提供的——比如运行shell脚本的Node API——当运行在远程时可能不会正常工作,因此我们建议你在所有的环境中测试一下你的插件。

测试和调试插件


这个部分将说明如何在远程目录下测试和调试插件。在这之前,我们先看一下怎么使用本地开发容器测试一个插件。本地测试容器是跨平台的,很容易部署,但是限制了访问文件系统的端口。由于只占用了非常小的OS空间,开发容器可以提供最为接近插件的真实运行环境。WSL,换句话说,就是一个典型的最小自治SSH主机。大部分场景下,你只要做小小的调整就可以解决问题了,相关主题查看常见问题。

安装开发版插件

目前,VS Code自动在SSH主机、容器、WSL安装插件时会使用插件市场的版本(而不是你本机上当前安装的版本)。大部分时候这么做事合理的,但是我们现在可能需要一个未发布的版本来测试,所以你可以将插件打包成VSIX格式,然后打开已经连接到远程VS Code窗口中手动安装这个插件。

遵循以下步骤:

  1. 如果这个插件已经发布,你需要将配置文件setting.json设置成"extensions.autoUpdate": false,阻止插件自动更新到插件市场的最新版本。
  2. 下一步,使用vsce package将你的插件打包成VSIX
  3. 连接到开发容器SSH主机WSL环境
  4. 在你已经连接远程目录的项目中,使用命令 Install from VSIX…安装你打包好的插件
  5. 完成后重启

?> 小提示:安装完毕后,你可以使用 Developer: Show Running Extensions命令查看VS Code在本地运行插件还是在远程运行插件的。

在远程调试你的插件

通常,你在本地机器上构建、编辑、加载和调试你的插件,远程调试插件也差不多遵循相同的模式,你只要把这些工作全部放在打开的远程开发目录去做就好了。

使用开发容器

遵循以下步骤开发和调试容器中的插件。

  1. 添加Node.js开发容器定义。在你的插件文件夹下按F1,选择 Remote-Containers: Create Configuration File… 命令,然后选择 Node.js 8 & Typescript(或者只是 Node.js 8)。这就定义了你将会编辑和调试的插件容器。
  2. 命令运行后,你可以自由修改.devcontainer文件夹,添加比如构建或运行时的其他选项。深入容器了解更多内容。
  3. 【可选】 编辑launch.jsonargs属性后面添加第二个参数,指向你的容器中的工作区目录下的测试项目/测试数据。比如,如果你的测试数据在工作区的一个data文件夹下,需要按照如下步骤添加${workspaceFolder}/data:

注意:你不可以单独使用${workspaceFolder}作为第二个参数

{
  "name": "Launch Extension",
  "type": "extensionHost",
  "request": "launch",
  "runtimeExecutable": "${execPath}",
  "args": ["--extensionDevelopmentPath=${workspaceFolder}", "${workspaceFolder}/data"],
  "stopOnEntry": false,
  "sourceMaps": true,
  "outFiles": ["${workspaceFolder}/dist/**/*.js"],
  "preLaunchTask": "npm"
}
  1. 运行 Remote-Containers: Reopen Folder in Container,VS Code会部署好容器,然后建立连接。现在你可以从容器内部修改源码了。
  2. 最后,按下F5或者使用 调试视图从容器内部加载然后启动调试器。
使用SSH 或 WSL

你在 SSH 主机WSL 中遵循类似的步骤。

  1. 使用SSH,你需要在远程主机上打开对应的项目(比如,使用 Remote-SSH: Connect to Host… 命令,然后在 File > Open打开对应的插件副本)。使用WSL,使用 File > New WSL Window然后 File > Open打开对应文件夹。
  2. 通过SSH主机/WSL打开文件夹后,你可以像在本地一样编辑源码了。
  3. 最后,按下 F5 或者使用 调试视图加载插件,像本地一样调试代码。

常见问题


VS Code API 会根据项目自动运行在正确的环境上。记住这点,然后我们来看看几个API,它会帮助你避免一些意外问题。

不正确的执行环境

如果你的插件出错了,它有可能是运行在了错误环境中。比较常见的场景是,你原本期望它运行在本地却运行在了远程上。你可以使用命令面板的 Developer: Show Running Extensions命令查看插件的运行情况。

如果这个命令显示某个UI 插件被当做工作区插件或者类似的情况,你可以试着在插件的package.json中设置extensionKind属性:

{
  "extensionKind": "ui"
}
  • "extensionKind": "ui" —— 将插件视为 UI 插件,强制在本地运行。
  • "extensionKind": "ui" —— 将插件视为 工作区插件,它有可能会在远程工作区的VS Code Server中运行。

你也可以在设置中修改 remote.extensionKind插件的类型,这项配置可以立竿见影地看到效果。比如,你想把 Azure Cosmos DB插件强制设置为 UI 插件(默认是工作区插件)然后把Debugger for Chrome设置为工作区插件(默认是UI 插件),你可以这么设置:

{
  "remote.extensionKind": {
    "ms-azuretools.vscode-cosmosdb": "ui",
    "msjsdiag.debugger-for-chrome": "workspace"
  }
}

使用 remote.extensionKind可以快速地测试发行版插件,而不用修改插件的package.json或重新构建插件。

保存插件数据或状态

有的时候,你的插件需要保留住不属于settings.json的数据或者独立的工作区配置文件(如.eslintrc),你可以使用插件激活入口的vscode.ExtensionContext对象。如果你的插件已经使用好了这些属性那就应该不会出错。

但是,你的插件如果依赖VS Code的路径约定(如 ~/.vscode)或者特定的OS目录(比如Linux上的 ~/.config/Code)来保存数据,那你就可能会遇到问题。不过还好这些小问题只要稍加修改插件就能处理掉。

如果你只是想保存一些键值对全局状态信息,你可以使用vscode.ExtensionContext.workspaceStatevscode.ExtensionContext.globalState。如果数据不止键值对且更为复杂,访问globalStoragePathstoragePath可以安全地获取对应的储存路径,供你读写文件。

这些API在1.31版本之后可用,你需要在package.json中配置版本:

{
  "engines": {
    "vscode": "^1.31.0"
  }
}

如何使用我们上面介绍的API:

import * as vscode from 'vscode';
import * as fs from 'fs';
import * as path from 'path';
export function activate(context: vscode.ExtensionContext) {
    context.subscriptions.push(
        vscode.commands.registerCommand('myAmazingExtension.persistWorkspaceData', () => {
        // 如果插件所属的工作区不存在储存路径,则创建一个
        if (!fs.existsSync(context.storagePath)) {
            fs.mkdirSync(context.storagePath);
        }
        // 将文件写入储存目录
        fs.writeFileSync(
            path.join(context.storagePath, 'workspace-data.json'),
            JSON.stringify({ now: Date.now() }));
    }));
    context.subscriptions.push(
        vscode.commands.registerCommand('myAmazingExtension.persistGlobalData', () => {
        // 如果插件所属的全局(跨工作区)文件夹不存在则创建一个
        if (!fs.existsSync(context.globalStoragePath)) {
            fs.mkdirSync(context.globalStoragePath);
        }
        // 为插件创建一个全局储存文件
        fs.writeFileSync(
            path.join(context.globalStoragePath, 'global-data.json'),
            JSON.stringify({ now: Date.now() }));
    }));
}

保留密钥

如果你的插件需要保留密码或者其他密钥,你可能会想到使用本地操作系统的密钥储存功能(Windows Cert Store、 macOS KeyChain、Linux 的 libsecret-based keyring),而不是使用远程主机的储存功能。更有可能的是,你可能需要在Linux上使用libsecret,在插件中使用gnome-keyring去储存你的密钥,通常来说,这项功能在服务端或者容器内不一定能运作起来。

VS Code本身不提供密钥储存机制,不过需要插件作者会转而使用keytar node module包。因此,VS Code内建了keytar,你在 工作区插件 中如果使用了它,它就会自动无声地运行在后台。这样一来你就可以使用本地系统的 keychain/ keyring/ cert store 同时还避免了各种问题。

比如:

import * as vscode from 'vscode';
function getCoreNodeModule(moduleName) {
  try {
    return require(`${vscode.env.appRoot}/node_modules.asar/${moduleName}`);
  } catch (err) {}
  try {
    return require(`${vscode.env.appRoot}/node_modules/${moduleName}`);
  } catch (err) {}
  return undefined;
}
// Use it
const keytar = getCoreNodeModule('keytar');
await keytar.setPassword('my-service-name', 'my-account', 'iamal337d00d');
const password = await keytar.getPassword('my-service-name', 'my-account');

使用剪贴板

由于历史原因,大部分插件作者会使用诸如clipboardy等Node.js的包和剪贴板交互。不幸的的是,如果你使用了这样的包,那么它就很有可能会运行在远程机器上。

VS Code 1.30引入的剪贴板则解决了这个问题。它总是运行在本地中,所以同样的,你只要修改你的engines.vscode版本就可以使用这个API了。

在插件中使用剪贴板API:

import * as vscode from 'vscode';
export function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('myAmazingExtension.clipboardIt', async () => {
      // 读取剪贴板
      const text = await vscode.env.clipboard.readText();
      // 写入剪贴板
      await vscode.env.clipboard.writeText(
        `It looks like you're copying "${text}". Would you like help?`
      );
    })
  );
}

在本地浏览器或者其他应用中打开些什么

在本地的场景下,通过使用子进程或者opn包启动浏览器或者其他应用是完全可行的,但是一旦插件运行到了远程上,这就会导致应用加载错误。VS Code远程开发部分兼容了opn包使得现有插件可以正常运行。你可以使用URI调用这个包,VS Code会带上这个URL在客户端唤起默认应用。由于不是完整实现,有些配置是不支持的,也不会返回child_process对象。

除了依赖第三方包,我们建议你使用vscode.env.openExternal方法在本地操作系统上启动默认应用打开对应的URI。而且vscode.env.openExternal还支持自动端口转发!你可以指向到远程的web server上,即使那个端口外部不可访问。

这项功能自1.31开始支持,所以你又要修改你的engines.vscode了。

如何使用vscode.env.openExternalAPI:

import * as vscode from 'vscode';
export async function activate(context: vscode.ExtensionContext) {
  context.subscriptions.push(
    vscode.commands.registerCommand('myAmazingExtension.openExternal', () => {
      // 示例 1 - 在默认浏览器中打开VS Code主页
      vscode.env.openExternal(vscode.Uri.parse('https://code.visualstudio.com'));
      // 示例 2 - 打开默认email应用
      vscode.env.openExternal(vscode.Uri.parse('mailto:vscode@microsoft.com'));
    })
  );
}

使用命令在插件间通信

一些插件意在被其他插件调用(通过vscode.extension.getExtension(extensionName).exports),因此会在激活时返回一些API。如果这些插件都在同一环境(都是UI插件,或者都是工作区插件时)下工作不会出什么问题,但是如果一个是UI插件,一个是工作区插件就会出问题了。

如果你的插件需要彼此产生交互,使用私有命令暴露功能可以避免一些问题。不过记住一点,所有你传入的对象参数都会被字符串化(JSON.stringify),因此这些对象不可以有循环引用,这会导致接受方只收到一个”字符串化的object类型”。

示例:

import * as vscode from 'vscode';
export async function activate(context: vscode.ExtensionContext) {
  // 注册私有命令
  const echoCommand = vscode.commands.registerCommand(
    '_private.command.called.echo',
    (value: string) => {
      return value;
    }
  );
  context.subscriptions.push(echoCommand);
}

使用Webview API


就像剪贴板API,Webview API也总是运行在本地环境,即使是 工作区插件 调用的。也就是说大部分基于webview的插件都可以正常工作,但是还有些注意事项需要交代一下。

访问localhost

默认情况下,webview中的localhost会解析到用户本地机器上,也就是说,远程运行的插件所启动的服务在webview里是无法访问的。即使你访问远程机器的ip,云主机或容器中的端口还是默认被拦截的。我们来看一下示意图

你可以用webview的message passingAPI绕过各种限制,你还可以 添加端口映射 告诉webview哪些端口可以直接转发到远程机器上。

端口映射可以将webview所使用的localhost端口映射到远程插件启动的任意端口上。如果你的 工作区插件 运行在远程,而且你也定义了端口映射,那么这些流量会自动、安全地转发到远程机器上。如果你的插件只是运行在本地,端口映射则会将一个localhost端口重新映射到另一个端口上。Webview端口映射同时支持UI插件工作区插件,也同时支持本地和远程环境。

这项功能自1.34开始支持,请修改你的package.json添加该支持。

使用端口映射,只要在你创建的webview中添加传入一个portMapping对象即可:

const STATIC_PORT = 3000;
const dynamicServerPort = getExpressServerPort();
const webviewPort = STATIC_PORT;
// 创建webview然后传入portMapping
const panel = vscode.window.createWebviewPanel(
  'remoteMappingExample',
  'Remote Mapping Example',
  vscode.ViewColumn.One,
  {
    portMapping: [
      // 这里映射了 webview 的 localhost:3000 到远程主机的 express 服务器端口
      { webviewPort: webviewPort, extensionHostPort: dynamicServerPort }
    ]
  }
);
// 你可以在HTML中使用"webviewPort"查看端口
panel.webview.html = `<!DOCTYPE html>
    <body>
        <!-- This will resolve to the dynamic server port on the remote machine -->
        <img src="http://localhost:${webviewPort}/canvas.png">
    </body>
    </html>`;

现在webview前往localhost:3000的流量都会通过VS Code的安全信道直接走到远程机器上。

使用原生Node.js模块


和插件打包(或动态引入的包)的原生node包会被Electorn的electron-rebuild重新编译。但是VS Code Server运行在一个标准的(非Electron)的Node.js中,因此可能造成远程二进制库失效问题。

解决这个问题需要:

  1. 同时引入Node.js和Elctron标准的两种二进制包(别忘了动态引入的包)。
  2. 检查vscode.extensions.getExtension('your.extensionId').extensionKind === vscode.ExtensionKind.Workspace是否根据环境使用了正确的包。
  3. 如果你想支持非x86_64构建目标和Alpine Linux则遵循下述类似逻辑。

使用VS Code的 Help > Developer Tools然后在控制台(console)中打印process.versions.modules可以找到VS Code使用的模块(modules)类型。如果你想要确保原生模块在各个Node.js环境中都能无缝运行,你可能把所有可能支持的平台(Electron Node.js, 官方Node.js Windows/Darwin/Linux的全部版本)相关的包全部引入。node-tree-sitter包在这方面是个非常好的例子。

为非x86_64主机或Apline Linux容器提供支持


如果你的插件只是用JavasSript/TypeScript写的,你的插件可能什么都不用做就能支持其他进程架构或基于musl的Apline Linux。

但是如果你的插件可以运行在Debian 9+, Ubuntu 16.04+, 或者基于 RHEL / CentOS 7+ 的远程SSH主机、容器或 WSL上,却无法支持非x86_64(比如 ARMv7l)或Alpine Linux容器,插件可能需要包含了x86_64的glibc特定机器码或运行时,所以导致了这些架构/操作系统上出现问题。

比如,你的插件坑包含了x86_64编译的原生模块或运行时版本。对于Alpine Linux来说就是因基础架构差异而无法运行这样的插件。

为了解决这个问题:

  1. 如果你是动态引入编译码,你可以用process.arch检测环境,然后根据对应架构下载对应架构下的依赖。如果你已经在插件中直接使用了二进制包,你也可以用同样的逻辑使用正确的包。
  2. 对于Alpine Linux来说,你可以用await fs.exists('/etc/alpine-release')检测操作系统,然后下载或者使用基于musl的正确的二进制包。
  3. 如果你不想支持这些平台,你也可以用同样的逻辑提供良好的错误提示。

你要非常注意一些第三方包可能依赖了导致这个问题的源码包。所以有时候你需要联系npm包作者提供额外的编译版本。

避免使用Electron模块


虽然依赖未暴露的内置Electron或者VS Code模块非常方便,但是你必须知道VS Code Server运作在标准的(非Electron)Node.js环境中,当插件运行在远程时就会丢失这些包。除了少数个例,比如keytar,用了特殊的实现所以在所有环境中都能正常工作。

使用基于Node.js的模块从而避免这些问题。如果你一定要用Electron模块,那就要确保如果包丢失的时候提供合适的后备方案。

下面的例子使用了Electron的original-fs包,缺失时则使用Node.js的fs模块。

function requireWithFallback(electronModule: string, nodeModule: string) {
  try {
    return require(electronModule);
  } catch (err) {}
  return require(nodeModule);
}
const fs = requireWithFallback('original-fs', 'fs');

但是不论何时,你都应该避免这些问题。

已知问题


目前我们还有些影响 工作区插件 功能的问题亟待解决。下表是已知的问题列表:

问题 描述
端口拦截 当使用Docker容器或者SSH服务器开发时端口不会自动转发,而插件中也没有可以程序性转发端口的API。虽然在使用Webview API中已经适配了webview,但是其他场景下还需要插件作者手动暴露端口
无法访问/传输远程工作区文件到本地 插件通过外部应用打开工作区文件会遇到错误,因为外部应用无法直接访问远程文件。我们正在调查插件怎样才能从远程工作区传输文件的相关方案。
无法从工作区插件访问关联设备 关联本地设备的插件无法在远程运行时关联对应设备,我们正在调查相关问题的最佳方案。

FAQ


使用不稳定的API

在VS Code中,插件API的兼容性非常重要,我们尽最大努力避免API变动以便插件开发人员已经发布的插件可以按预期工作。但是,这对使得我们束手束脚,一旦新的API发布后,就不可能再轻易改动。

不稳定的API(Proposed API)则解决了这个问题,不稳定的API是VS Code已经实现但是还未公开的API。它们只在Insider版的VS Code中可用,而且很有可能再次产生变动,你也不能在想要发布的插件中使用。不管怎样,插件开发者开始可以在本地开发时测试这些API,然后给VS Code团队提供建议的,经过不断修改之后最终可能出现在稳定版中。

使用不稳定的API

下面是在本地开发中测试未稳定API的步骤:

  • 使用 VS Code的Insider版本
  • package.json中添加"enableProposedApi": true
  • 把最新的vscode.proposed.d.ts复制到你的项目中

参考

VS Code API

VS Code API是VS Code提供给插件使用的一系列Javascript API。

!> 注意:VS Code API 变动较快,请自行参考官方文档,其中包含了完整而且是最新的VS Code API列表。

API模式


以下将介绍我们在VS Code中经常使用的API模式。

Promises(异步)

VS Code API完全采用了promise的实现。对于插件来说允许任何promise形式的返回格式,如ES6,WinJS,A+等。

一个promise库需要它的API使用Thenable类型来表达,Thenable类型代表了一种通用特性的实现——then方法。

大多数时候promise是一个可选项,VS Code调用插件之后,它能直接处理正常的返回结果也能处理Thenable的结果类型。当promise是可选的API返回结果时,API会在返回类型中用Thenable表示。

provideNumber(): number | Thenable<number>

Cancellation Tokens(取消式令牌)

有些事件可能从不稳定的变化状态开始,而随着状态变化这一事件最后肯能被取消了。比如:IntelliSense(智能补全)被触发后,用户持续输入的行为使得这一操作最终被取消了。

API也为这种行为提供了解决方案,你可以通过CancellationToken检查取消的状态(isCancellationRequested)或者当取消发生时得到通知(onCancellationRequested)。取消式令牌通常是API函数的最后一个(可选)参数。

Disposables(释放器)

VS Code API使用了释放器模式,这个模式被用于事件侦听,命令,UI交互和各类语言配置上。

例如:setStatusBarMessage(value: string)事件返回一个Disposable对象,这个对象最终调用dispose方法移除message。

Events(事件)

事件在API中被暴露为函数,当你订阅一个事件侦听器时绑定。事件侦听器调用后会返回一个Disposable,它会在dispose触发后,移除事件侦听器。

var listener = function(event) {
    console.log("It happened", event);
};
// 开始侦听
var subscription = fsWatcher.onDidDelete(listener);
// 你的代码
subscription.dispose(); // 停止侦听

事件的命名规则遵循on[Will | Did] 动词 + 名词的形式。通过onWill表示将要发生,onDid表示已经发生,动词表示行为,名词指代上下文或目标。

举个栗子:window.onDidChangeActiveTextEditor中,激活的编辑器(ActiveTextEditor:名词)变动(change:动词)后(onDid)会触发事件。

严格null检查

VS CodeAPI使用undefinednull的Typescript类型,同样也支持严格null检查

发布内容配置

发布内容配置(即VS Code为插件扩展提供的配置项)是pacakge.json插件清单contributes字段,你可以在其中注册各种配置项扩展VS Code的能力。下面是目前可用的配置项列表:

  • configuration
  • commands
  • menus
  • keybindings
  • languages
  • debuggers
  • breakpoints
  • grammars
  • themes
  • snippets
  • jsonValidation
  • views
  • viewsContainers
  • problemMatchers
  • problemPatterns
  • taskDefinitions
  • colors
  • typescriptServerPlugins

contributes.configuration


在configuration中配置的内容会暴露给用户,用户可以从“用户设置”和“工作区设置”中修改你暴露的选项。

configuration是JSON格式的键值对,用户会在修改设置时获得对应的提示和更好的体验。

你可以用vscode.workspace.getConfiguration('myExtension')读取配置值。

?> 小提示:配置markdownDescription比配置description更好,它能呈现Markdown格式的文档。

示例

"contributes": {
    "configuration": {
        "type": "object",
        "title": "TypeScript configuration",
        "properties": {
            "typescript.useCodeSnippetsOnMethodSuggest": {
                "type": "boolean",
                "default": false,
                "description": "Complete functions with their parameter signature."
            },
            "typescript.tsdk": {
                "type": ["string", "null"],
                "default": null,
                "description": "Specifies the folder path containing the tsserver and lib*.d.ts files to use."
            }
        }
    }
}

contributes.configurationDefaults


为特定的语言配置编辑器的默认值,修改这个配置会覆盖编辑器已经为语言提供的默认配置。

下面的示例是修改markdown语言的默认配置。

示例

"contributes": {
    "configurationDefaults": {
        "[markdown]": {
            "editor.wordWrap": "on",
            "editor.quickSuggestions": false
        }
    }
}

contributes.commands


设置命令标题和命令体,随后这个命令会显示在命令面板中。你也可以加上category前缀,在命令面板中会以分类显示。

?>注意:当调用命令时(通过组合键或者在命令面板中调用),VS Code会触发激活事件onCommand:${command}

下面的示例是修改markdown语言的默认配置。

示例

"contributes": {
    "commands": [{
        "command": "extension.sayHello",
        "title": "Hello World",
        "category": "Hello"
    }]
}

?> 提示: 想了解更多的有关于在VS Code插件开发中使用命令, 请参阅命令章节

contributes.menus


为编辑器或者文件管理器设置命令的菜单项。菜单项至少包含1️⃣选中时调用的命令和2️⃣何时显示这个菜单项的时机。显示菜单的时机由when键定义,而对应的值语法需要参考键值绑定的when语法

command键则是必须的。可选的命令使用alt定义,当你按下ALT键时,菜单中会显示对应的菜单项。

最后,group属性定义了菜单的分组。navigation值不同于普通的group值,一旦设置这个值就会总是显示在菜单的最顶端。

当前插件创作者可以配置的菜单的地方有:

  • 全局命令面板 - commandPalette
  • 资源管理器上下文菜单 - explorer/context
  • 编辑器上下文菜单 - editor/context
  • 编辑器标题栏 - editor/title
  • 编辑器标题上下文菜单 - editor/title/context
  • 调试栈视图的上下文菜单 - debug/callstack/context
  • SCM 标题菜单 - scm/title
  • SCM 资源组 - scm/resourceGroup/context
  • SCM 资源 - scm/resource/context
  • SCM 改变标题 - scm/change/title
  • 视图的标题菜单 - view/title
  • 视图项的菜单 - view/item/context

?>注意:当菜单中的命令被调用,VS Code会将当前选中资源作为参数传给调用的命令。比方说,资源管理器的菜单被触发,选中资源的URI会作为参数,编辑器中的菜单项被触发,则将当前文件的URI作为参数传入。

关于标题还有一点要说,命令还可以定义图标,VS Code会显示在编辑器的标题菜单栏中。

示例

"contributes": {
    "menus": {
        "editor/title": [{
            "when": "resourceLangId == markdown",
            "command": "markdown.showPreview",
            "alt": "markdown.showPreviewToSide",
            "group": "navigation"
        }]
    }
}

让菜单项只显示在命令面板中

注册的命令默认显示在命令面板中。要想控制命令的可见性,我们提供了一个commandPalette菜单配置,在这个配置中,你可以定义一个when控制是否在命令菜单中显示。

下面的片段只在编辑器中选中了什么东西的时候才会在命令面板中显示出‘Hello World’:

"commands": [{
    "command": "extension.sayHello",
    "title": "Hello World"
}],
"menus": {
    "commandPalette": [{
        "command": "extension.sayHello",
        "when": "editorHasSelection"
    }]
}

分组排序

菜单项可以通过组来分类。根据下列默认规则,然后按照字母排序,

编辑器上下文菜单默认有这些分组:

  • navigation - navigation组始终在最上方。
  • 1_modification - 紧接上一个组,这个组包含可以修改你代码的命令。
  • 9_cutcopypaste - 然后是基础编辑命令。
  • z_commands - 最后一个分组则是命令面板入口。

资源管理器上下文菜单默认有下列分组:

  • navigation - 在VS Code中导航的相关命令。navigation组始终在最上方。
  • 2_workspace - 和工作区操作相关的命令。
  • 3_compare - 比较文件和diff相关的命令。
  • 4_search - 在搜索视图中和搜索相关的命令。
  • 5_cutcopypaste - 和剪切、复制、粘贴文件相关的命令。
  • 7_modification - 修改文件的相关命令。

编辑器标签菜单默认有下列分组

  • 1_close - 和关闭编辑器相关的命令。
  • 3_preview - 和固定编辑器相关的命令。

编辑器标题菜单默认有下列分组

  • 1_diff - diff编辑器相关的命令。
  • 3_open - 打开编辑器的相关命令。
  • 5_close - 和关闭编辑器相关的命令。

组内排序

组内的菜单顺序取决于标题或者序号属性。菜单的组内顺序由@<number>加到group值的后面得以确定:

"editor/title": [{
    "when": "editorHasSelection",
    "command": "extension.Command",
    "group": "myGroup@1"
}]

contributes.keybindings


这个配置确定了用户输入按键组合时的触发规则。在快捷键绑定中,你可以了解更加细节的东西。

配置快捷键绑定会使默认键盘快捷方式中显示你的规则,每一处和命令相关的UI部分也会显示你添加的快捷键组合。

?>注意因为VS Code支持Windows,macOS和Linux平台,而

示例

Windows和Linux下使用Ctrl+F1,macOS下使用Cmd+F1调用"extension.sayHello"命令:

"contributes": {
    "keybindings": [{
        "command": "extension.sayHello",
        "key": "ctrl+f1",
        "mac": "cmd+f1",
        "when": "editorTextFocus"
    }]
}

contributes.languages


配置一门语言,引入一门新的语言或者加强VS Code已有的语言支持。

在这部分内容中,一个语言必须要有一个标识符(identifier)关联到文件上(查看 TextDocument.getLanguageId())。

VS Code提供三种文件应该关联哪种语言的方式。每种方式都可以可以“单独”加强:

  1. 插件的文件名
  2. 文件名
  3. 文件内的首行

用户打开文件后,三种规则都会使用,然后确定语言。接着VS Code就会触发激活事件onLanguage:${language}(比如:下面的onLanguage:python例子)

aliases属性包含着这门语言的可读性名称。这个列表的第一项会作为语言标签(在VS Code右下角状态栏显示)。

configuration属性确定了语言配置文件的路径。路径是指相对插件文件夹的路径,通常是./language-configuration.json,这个文件是JSON格式的,包含着下列可配置属性:

  • comments

    - 定义了注释的符号

    • blockComment - 用于标识块注释的起始和结束token。被’Toggle Block Comment’使用
    • lineComment - 用于标识行注释的起始token。被’Add Line Comment’使用
  • brackets - 定义括号,同时也会影响括号内的代码缩进。进入新的一行时,被编辑器用来确定或是更正新的缩进距离

  • autoClosingPairs - 为自动闭合功能定义某个符号的开闭符(open and close symbols)。开符号输入后,编辑器会自动插入闭符号。使用notIn参数,关闭字符串或者注释中的符号对

  • surroundingPairs - 定义选中文本的开闭符号

  • folding

    - 定义编辑器中的代码应何时、应怎么样折叠

    • offSide - 和一下个缩进块之间的代码块尾部的空行(用于基于缩进的语言,如Python or F#)
    • markers - 使用正则自定义代码中的折叠区域标识符
  • wordPattern - 使用正则匹配编程语言中哪些词应该是单个词

如果你的语言配置文件是language-configuration.json,或者以这样的字符串结尾的,VS Code就会提供校验和编辑支持。

示例

...
"contributes": {
    "languages": [{
        "id": "python",
        "extensions": [ ".py" ],
        "aliases": [ "Python", "py" ],
        "filenames": [ ... ],
        "firstLine": "^#!/.*\\bpython[0-9.-]*\\b",
        "configuration": "./language-configuration.json"
    }]
}

language-configuration.json

{
    "comments": {
        "lineComment": "//",
        "blockComment": [ "/*", "*/" ]
    },
    "brackets": [
        ["{", "}"],
        ["[", "]"],
        ["(", ")"]
    ],
    "autoClosingPairs": [
        ["{", "}"],
        ["[", "]"],
        ["(", ")"],
        { "open": "'", "close": "'", "notIn": ["string", "comment"] },
        { "open": "/**", "close": " */", "notIn": ["string"] }
    ],
    "surroundingPairs": [
        ["{", "}"],
        ["[", "]"],
        ["(", ")"],
        ["<", ">"],
        ["'", "'"]
    ],
    "folding": {
        "offSide": true,
        "markers": {
            "start": "^\\s*//#region",
            "end": "^\\s*//#endregion"
        }
    },
    "wordPattern": "(-?\\d*\\.\\d\\w*)|([^\\`\\~\\!\\@\\#\\%\\^\\&\\*\\(\\)\\-\\=\\+\\[\\{\\]\\}\\\\\\|\\;\\:\\'\\\"\\,\\.\\<\\>\\/\\?\\s]+)"
}

contributes.debuggers


配置VS Code的调试器,调试器配置有下列属性:

  • type 用于加载配置的调试器唯一标识——ID。

  • label 会在UI中显示的调试器名称。

  • program 调试适配的路径,调试适配通过VS Code debug protocol连接到真正的调试器或者运行时。

  • runtime 如果调试适配器的路径不是可执行程序,那么就会用到这个运行时。

  • configurationAttributes 调试器的启动配置参数。

  • initialConfigurations 列出了初始化launch.json需要的加载配置。

  • configurationSnippets 列出了编辑launch.json文件时可以提供的加载配置智能提示。

  • variables 引入替代变量,并绑定到调试器插件实现的命令上。

  • languages 调试插件会使用“默认调试器”的语言

  • adapterExecutableCommand调试适配器执行路径和参数动态计算的命令。命令返回的格式如下:

    command: "<executable>",
    args: [ "<argument1>", "<argument2>", ... ]

    command属性必须是一个可执行程序的绝对路径,或者是通过PATH环境变量可以查找到可执行程序的名称。使用特殊值node,则会映射到VS Code内建的node运行时,而不会在PATH中查找。

示例

"contributes": {
    "debuggers": [{
        "type": "node",
        "label": "Node Debug",
        "program": "./out/node/nodeDebug.js",
        "runtime": "node",
        "languages": ["javascript", "typescript", "javascriptreact", "typescriptreact"],
        "configurationAttributes": {
            "launch": {
                "required": [ "program" ],
                "properties": {
                    "program": {
                        "type": "string",
                        "description": "The program to debug."
                    }
                }
            }
        },
        "initialConfigurations": [{
            "type": "node",
            "request": "launch",
            "name": "Launch Program",
            "program": "${workspaceFolder}/app.js"
        }],
        "configurationSnippets": [
            {
                "label": "Node.js: Attach Configuration",
                "description": "A new configuration for attaching to a running node program.",
                "body": {
                    "type": "node",
                    "request": "attach",
                    "name": "${2:Attach to Port}",
                    "port": 9229
                }
            }
        ],
        "variables": {
            "PickProcess": "extension.node-debug.pickNodeProcess"
        }
    }]
}

contributes.breakpoints


通常调试器插件会有contributes.breakpoints入口,插件可以在这里面设置哪些语言可以设置断点。

"contributes": {
    "breakpoints": [
        {
            "language": "javascript"
        },
        {
            "language": "javascriptreact"
        }
    ]
}

contributes.grammars


为一门语言配置TextMate语法。你必须提供应用语法的language,TextMate的scopeName确定了语法和文件路径。

!>注意:包含语法的文件必须是JSON(以.json结尾的文件)或者XML的plist格式文件。

示例

"contributes": {
    "grammars": [{
        "language": "markdown",
        "scopeName": "text.html.markdown",
        "path": "./syntaxes/markdown.tmLanguage.json",
        "embeddedLanguages": {
            "meta.embedded.block.frontmatter": "yaml",
            ...
        }
    }]
}

查看语法高亮指南学习更多从现有的语法高亮插件迁移的内容。

contributes.themes


为VS Code添加TextMate主题。你必须添加一个label,指定这个主题是dark还是light的(以便VS Code根据你的主题调整界面),当然还需要加上目标文件路径(XML plist 格式)。

!>注意:包含语法的文件必须是JSON(以.json结尾的文件)或者XML的plist格式文件。

示例

"contributes": {
    "themes": [{
        "label": "Monokai",
        "uiTheme": "vs-dark",
        "path": "./themes/Monokai.tmTheme"
    }]
}

查看改变色彩主题学习使用yo code插件生成器将TextMate.tmTheme文件快速打包成VS Code插件。

contributes.snippets


为语言添加代码片段。language属性必须是语言标识符path则必须是使用VS Code代码片段格式的代码片段文件的相对路径。

示例

下面是一个Go语言的代码片段:

"contributes": {
    "snippets": [{
        "language": "go",
        "path": "./snippets/go.json"
    }]
}

contributes.jsonValidation


json文件添加校验器。url值可以是本地路径也可以是插件中的模式文件(schema file),或者是远程服务器的URL比如:json schema

示例

"contributes": {
    "jsonValidation": [{
        "fileMatch": ".jshintrc",
        "url": "http://json.schemastore.org/jshintrc"
    }]
}

contributes.views


为VS Code 添加视图。你需要为视图指定唯一标识和名称。可以配置的属性如下:

  • explorer: 活动栏中的资源管理视图容器。
  • scm: 活动栏中的源代码管理(SCM) 视图容器。
  • debug: 活动栏中的调试视图容器。
  • test: 活动栏中的测试视图容器。
  • Custom view containers 由插件提供的自定义视图容器。

当用户打开视图,VS Code会触发onView:${viewId}激活事件(比如:下面示例中的onView:nodeDependencies)。你也可以用when控制视图的可见性。

示例

"contributes": {
    "views": {
        "explorer": [
            {
                "id": "nodeDependencies",
                "name": "Node Dependencies",
                "when": "workspaceHasPackageJSON"
            }
        ]
    }
}

插件创作者应该通过createTreeViewAPI提供的data provider创建一个TreeView或者直接使用registerTreeDataProvider注册一个data provider。更多示例参考这里

contributes.viewsContainers


配置自定义视图的视图容器。你需要为视图指定唯一标识和标题和图标。目前你只可以配置活动栏(activitybar),下面的示例展示了活动栏中的Package Explorer视图容器应该如何配置。

示例

"contributes": {
    "viewsContainers": {
        "activitybar": [
            {
                "id": "package-explorer",
                "title": "Package Explorer",
                "icon": "resources/package-explorer.svg"
            }
        ]
    },
    "views": {
        "package-explorer": [
            {
                "id": "package-dependencies",
                "name": "Dependencies"
            },
            {
                "id": "package-outline",
                "name": "Outline"
            }
        ]
    }
}

图标规格

  • Size: 28x28的图标居中于50x40的视图块上。
  • Color: 图标应使用黑白单色。
  • Format: 虽然图片格式的图标都是可以的,但建议使用SVG图标。
  • States: 所有图标状态继承下列样式:
State Opacity
Default 60%
Hover 100%
Active 100%

contributes.problemMatchers


配置问题定位器的模式。这些配置在输出面板和终端中都会有所体现,下面是一个配置了插件中的gcc编译器的问题定位器示例:

示例

"contributes": {
    "problemMatchers": [
        {
            "name": "gcc",
            "owner": "cpp",
            "fileLocation": ["relative", "${workspaceFolder}"],
            "pattern": {
                "regexp": "^(.*):(\\d+):(\\d+):\\s+(warning|error):\\s+(.*)$",
                "file": 1,
                "line": 2,
                "column": 3,
                "severity": 4,
                "message": 5
            }
        }
    ]
}

这个问题定位器现在可以通过名称引用$gcctask.json中使用了,示例如下:

{
    "version": "2.0.0",
    "tasks": [
        {
            "label": "build",
            "command": "gcc",
            "args": ["-Wall", "helloWorld.c", "-o", "helloWorld"],
            "problemMatcher": "$gcc"
        }
    ]
}

更多内容请查看:实现一个问题定位器

contributes.problemPatterns


配置可以在问题定位器(见上)中可以使用的问题模式的名称。

contributes.taskDefinitions


配置和定义一个object结构,定义系统中唯一的配置任务。任务定义最少需要一个type属性,不过通常需要更多的属性配置。 在package.json文件中,一个展示脚本的任务看起来是这样的:

"taskDefinitions": [
    {
        "type": "npm",
        "required": [
            "script"
        ],
        "properties": {
            "script": {
                "type": "string",
                "description": "The script to execute"
            },
            "path": {
                "type": "string",
                "description": "The path to the package.json file. If omitted the package.json in the root of the workspace folder is used."
            }
        }
    }
]

任务定义是JSON格式的,且包含requiredproperties两个属性。 type属性定义了任务类型,如果上述例子变成:

  • "type": "npm"要求任务与npm任务相关联
  • "required": [ "script" ]其中script属性不可或缺。path属性变成可选。
  • "properties": {...}:定义了其他属性和他们的类型

当插件真的创建了一个任务,它需要传入一个与package.json中任务配置对应的TaskDefinition。对于npm任务来说,pacakge.json中的脚本应该是这样的:

let task = new vscode.Task({ type: 'npm', script: 'test' }, ....);

contributes.colors


这些色彩可用于状态栏的编辑器装饰器。定义之后,用户可以在workspace.colorCustomization设置中自定义颜色,用户的主题会覆盖这些色值。

"contributes": {
  "colors": [{
      "id": "superstatus.error",
      "description": "Color for error message in the status bar.",
      "defaults": {
          "dark": "errorForeground",
          "light": "errorForeground",
          "highContrast": "#010203"
      }
  }]
}

contributes.typescriptServerPlugins


配置VS Code的Javascript和Typescript支持的Typescript 服务器插件

"contributes": {
   "typescriptServerPlugins": [
      {
        "name": "typescript-styled-plugin"
      }
    ]
}

上述例子配置了typescript-styled-plugin,这个插件为Javascript和Typescript添加了风格化的组件智能提示。这个插件会从扩展插件中加载,而且必须在dependency中列明:

{
    "dependencies": {
        "typescript-styled-plugin": "*"
    }
}

Typescript 服务器插件可以被所有Javascript和Typescript文件加载,只有当用户的工作区使用Typescript时才会激活。

激活事件

激活事件是在package.json中的activationEvents字段声明的一个JSON对象, 参考插件清单. 当激活事件触发时, 插件就会被激活. 下面是可用的激活事件列表:

  • onLanguage
  • onCommand
  • onDebug
    • onDebugInitialConfigurations
    • onDebugResolve
  • workspaceContains
  • onFileSystem
  • onView
  • onUri
  • onWebviewPanel
  • *

package.json的配置项都可以在插件清单中找到.

onLanguage


打开特定语言文件时激活事件和相关插件

...
"activationEvents": [
  "onLanguage:python"
]
...

onLanguage事件需要指定特定的语言标识符

也可以添加多种语言:

"activationEvents": [
  "onLanguage:json",
  "onLanguage:markdown",
  "onLanguage:typescript"
]

onCommand


调用命令时激活

...
"activationEvents": [
  "onCommand:extension.sayHello"
]
...

onDebug


调试会话(debug session)启动前激活

...
"activationEvents": [
  "onDebug"
]
...

onDebugInitialConfigurations

onDebugResolve

这是两个粒度更细的onDebug激活事件:

  • DebugConfigurationProvider中的provideDebugConfigurationsonDebugInitialConfigurations之后触发
  • onDebugResolve:typeDebugConfigurationProviderresolveDebugConfiguration方法之前触发.

友情提示: 如果调试插件比较轻量, 使用onDebug. 相反, 根据DebugConfigurationProvider实现的对应方法(provideDebugConfigurationsresolveDebugConfiguration),使用onDebugInitialConfigurationsonDebugResolve. 参阅使用调试器插件.

workspaceContains


文件夹打开后,且文件夹中至少包含一个符合glob模式的文件时触发.

"activationEvents": [
  "workspaceContains:**/.editorconfig"
]

onFileSystem


以协议(scheme)打开文件或文件夹时触发。通常是file-协议,也可以用自定义的文件供应器函数替换掉,比如ftpssh.

...
"activationEvents": [
  "onFileSystem:sftp"
]
...

onView


指定id的视图展开时触发:

...
"activationEvents": [
  "onView:nodeDependencies"
]
...

onUri


插件的系统级URI打开时触发。这个URI协议需要带上vscode或者 vscode-insiders协议。URI主机名必须是插件的唯一标识,剩余的URI是可选的。

...
"activationEvents": [
  "onUri"
]
...

如果vscode.git插件定义了onUri激活事件,那么下列任意URI打开时就会触发:

  • vscode://vscode.git/init
  • vscode://vscode.git/clone?url=https%3A%2F%2Fgithub.com%2FMicrosoft%2Fvscode-vsce.git
  • vscode-insiders://vscode.git/init(for VS Code Insiders)

onWebviewPanel


VS Code需要恢复匹配到viewTypewebview视图时触发.

下面是一个例子:

"activationEvents": [
  ...,
  "onWebviewPanel:catCoding"
]

这会导致插件被激活. 调用window.createWebviewPanel时可以设置viewType, 你可能会需要其它的激活事件(比如: onCommand)来创建webview视图.

Start up


当VS Code启动时触发。为了保证良好的用户体验,只在你的插件没有其他任何激活事件的前提下,添加这个激活事件。

...
"activationEvents": [
  "*"
]
...

!> 注意: 一个插件如果侦听了多个激活事件, 那么最好用"*"替换掉.

!> 注意: 插件必须从它的主模块中输出一个activate()函数,当任意的激活事件触发时,VS Code会仅仅调用一次这个函数。此外,插件也应该 导出一个deactivate()函数,当VS Code关闭时执行清理的任务。如果清理进程是异步的,插件的deactivate()必须返回一个Promise。如果这个清理任务是同步的,那么deactivate()可以返回undefined

插件清单

配置字段


名称 必须 类型 详细
name Y string 插件的名称必须用全小写无空格的字母组成。
version Y string SemVer版本模式兼容。
publisher Y string 发行方名称
engines Y object 一个至少包含vscode字段的对象,其值必须兼容 VS Code版本。不可以是*。例如:^0.10.5 表明最小兼容0.10.5版本的VS Code。
license string 参考npm’s documentation。如果你在插件根目录已经提供了LICENSE文件。那么license的值应该是"SEE LICENSE IN <filename>"
displayName string 插件市场所显示的插件名称。
description string 简单地描述一下你的插件是做什么的。
categories string[] 你想要使用的插件分类,可选值有:[Programming Languages, Snippets, Linters, Themes, Debuggers, Formatters, Keymaps, SCM Providers, Other, Extension Packs, Language Packs]
keywords array 关键字(数组),这样用户可以更方便地找到你的插件。到时候会和市场上的其他插件以标签筛选在一起。
galleryBanner object 根据你的icon格式化市场的头部显示。详情见下。
preview boolean 在市场中会显示Preview标记。
main string 你的插件入口
contributes object 描述插件发布内容的对象。
activationEvents array 激活事件数组。
badges array 显示在插件市场页面侧边栏的合法标记。 每个标记都是一个对象,包含了3个属性:url 标记的图片URL,当用户点击标记和description时,会跳转到href
markdown string 控制市场中使用的Markdown渲染引擎。可以是github (默认) 或 standard
qna marketplace (默认), string, false 控制市场中的Q & A 链接。 设置成marketplace时,自动使用市场默认的Q & A网址。或者提供一个URL转跳到你的Q & A 地址。设置为false时禁用。
dependencies object Node.js 运行时依赖。等同于npm’s dependencies.
devDependencies object Node.js 开发时依赖。 等同于npm’s devDependencies.
extensionDependencies array 插件依赖,由插件ID组成的数组。当主要插件安装完成后,其他插件会相应安装。插件ID的格式为 ${publisher}.${name}。比如:vscode.csharp
scripts object 等同于npm的 scripts,不过有VS Code额外字段如vscode:prepublish或vscode:uninstall.
icon string icon的文件路径,最小 128x128 像素 (视网膜屏幕则需 256x256)。

你还可以参考npm的package.json

示例


下面是一份完整的package.json示例

{
    "name": "wordcount",
    "displayName": "Word Count",
    "version": "0.1.0",
    "publisher": "ms-vscode",
    "description": "Markdown Word Count Example - reports out the number of words in a Markdown file.",
    "author": {
        "name": "seanmcbreen"
    },
    "categories": [
        "Other"
    ],
    "icon": "images/icon.png",
    "galleryBanner": {
        "color": "#C80000",
        "theme": "dark"
    },
    "activationEvents": [
        "onLanguage:markdown"
    ],
    "engines": {
        "vscode": "^1.0.0"
    },
    "main": "./out/extension",
    "scripts": {
        "vscode:prepublish": "node ./node_modules/vscode/bin/compile",
        "compile": "node ./node_modules/vscode/bin/compile -watch -p ./"
    },
    "devDependencies": {
        "vscode": "0.10.x",
        "typescript": "^1.6.2"
    },
    "license": "SEE LICENSE IN LICENSE.txt",
    "bugs": {
        "url": "https://github.com/Microsoft/vscode-wordcount/issues",
        "email": "smcbreen@microsoft.com"
    },
    "repository": {
        "type": "git",
        "url": "https://github.com/Microsoft/vscode-wordcount.git"
    },
    "homepage": "https://github.com/Microsoft/vscode-wordcount/blob/master/README.md"
}

插件市场展示小贴士


下面是一些让你的插件在市场上看起来狂拽酷帅吊炸天的小建议。

使用npm install -g vsce安装最新的vsce

在插件根目录中新建一个README.md文件,我们会把里面的内容作为插件的介绍(在市场上),你可以在README.md提供图片的相对路径。

下面是两个栗子🌰:

  1. Word Count
  2. MD Tools

好的名字和描述是市场展示产品非常重要的部分。下述字符串用于VS Code文本搜索,带上关键字更容易被找到。

    "displayName": "Word Count",
    "description": "Markdown Word Count Example - reports out the number of words in a Markdown file.",

Icon和banner颜色会展示在市场页面头部,theme属性是指banner中使用的字体主题——darklight

{
    "icon": "images/icon.png",
    "galleryBanner": {
        "color": "#C80000",
        "theme": "dark"
    },
}

下面的几个可选链接(bugshomepagerepository)会在市场的Resources部分显示:

{
    "license": "SEE LICENSE IN LICENSE.txt",
    "homepage": "https://github.com/Microsoft/vscode-wordcount/blob/master/README.md",
    "bugs": {
        "url": "https://github.com/Microsoft/vscode-wordcount/issues",
        "email": "smcbreen@microsoft.com"
    },
    "repository": {
        "type": "git",
        "url": "https://github.com/Microsoft/vscode-wordcount.git"
    },
}
市场资源链接 对应的package.json属性
Issues bugs:url
Repository repository:url
Homepage homepage
License license

设置插件的categorycategory一样的插件会分类到一起以便用户查找和筛选。

注意:请使用有意义的分类值,允许的值有[Programming Languages, Snippets, Linters, Themes, Debuggers, Formatters, Keymaps, SCM Providers, Other, Extension Packs, Language Packs]。有语法高亮、代码补全功能的插件,请使用Programming LanguagesLanguage Packs分类是为本地化保留的插件类别(例如:简体中文(本地化))。

{
    "categories": [
        "Linters", "Programming Languages", "Other"
    ],
}

?> 小贴士: The Extension Manifest Editor 插件可以帮你预览预览你的插件中的README.mdpackage.json, 生成的预览就像你已经发布到插件市场了一样。

使用认证过的徽章

出于安全考虑,我们只允许可信服务商提供的标志。 我们允许来自下列URL前缀的标志:

如果你想用其他标志,欢迎在我们的Github issue页面提供建议。

整合插件配置


yo code可以帮你轻松地打包TextMate 主题,着色器,代码片段和创建新插件。当你运行了生成器,每一次配置都会创建一个完整、独立的插件包。但是,将多个配置内容整合进一个插件会更方便。比如:你想要支持一门新的语言,你会希望同时提供语法高亮和代码片段,甚至调试支持。

为了整合插件配置,编辑已有的package.json文件,然后添加新的配置内容,关联相关文件。

下面是一个包含了LaTex语言定义(语言标识符和相关文件插件),(语法)着色器和代码片段。

{
    "name": "language-latex",
    "description": "LaTex Language Support",
    "version": "0.0.1",
    "publisher": "someone",
    "engines": {
        "vscode": "0.10.x"
    },
    "categories": [
        "Programming Languages",
        "Snippets"
    ],
    "contributes": {
        "languages": [{
            "id": "latex",
            "aliases": ["LaTeX", "latex"],
            "extensions": [".tex"]
        }],
        "grammars": [{
            "language": "latex",
            "scopeName": "text.tex.latex",
            "path": "./syntaxes/latex.tmLanguage.json"
        }],
        "snippets": [{
            "language": "latex",
            "path": "./snippets/snippets.json"
        }]
    }
}

注意插件的categories字段现在包含了Programming LanguagesSnippets,以便用户在市场中找到这个插件。

?> 小贴士: 整合好的配置文件应该使用同样的标识符。在上述例子中,所有的标识符都用了”latex”。这样VS Code 才知道(语法)着色器和代码片段是为LaTeX语言准备的,当编辑LaTeX文件的时候才会激活插件。

插件包


你也可以将几个独立的插件打包成一个“插件包”。插件包是指一组可以无冲突安装的插件集合。然后你就可以很方便地把插件分享给其他人,或者为特定情境创建一组插件,比如帮助PHP工程师在VS Code中快速上手。

一个插件包可以包含其他插件,或者直接将其打包到自身中。package.json中的extensionDependencies描述了这项依赖。

举个例子🌰,下面是一个PHP插件包,其中包含了调试器,语言服务器和格式化器。

{
  "extensionDependencies": [
      "felixfbecker.php-debug",
      "felixfbecker.php-intellisense",
      "Kasik96.format-php"
  ]
}

当安装插件包的时候,VS Code会连同它的插件依赖一起安装。

插件包需要使用市场分类中的Extension Packs

{
  "categories": [
      "Extension Packs"
  ],
}

想要创建插件包,你可以使用yo codeYeoman生成器。另外,你也可以用你VS Code中已有的一些插件生成插件包,然后你就可以很轻松地从喜欢的插件中创建出插件包,再发布到市场上或者分享给其他用户。

插件包不应该有除了它内部打包之外的其他插件包,打包好的插件包应该是在整个包里面可以独立管理的。如果一个插件非常依赖另外一个插件,那么这个依赖性应该在extensionDependencies中声明。

插件卸载钩子


如果你的插件在删除时需要做一些清理工作,你可以在package.json中的卸载钩子vscode:uninstall中注册一个node脚本。

{
  "scripts": {
    "vscode:uninstall": "node ./out/src/lifecycle"
  }
}

这个脚本会在插件完全卸载之后执行,也就是插件完全卸载之后——VS Code重载(关闭然后启动)之后执行。

!> 注意:只支持Node.js脚本

下面有几个npmjs的Node.js 模块,可以帮你实现VS Code插件。你可以在插件的dependencies部分包含进去。

内置命令

这篇文档列出了可能需要与vscode.commands.executeCommand一起使用的命令集合.

阅读命令指南以了解如何使用commandsAPI.

下面是一个如何在 VS Code 中打开新文件夹的例子:

let uri = Uri.file('/some/path/to/folder');
let success = await commands.executeCommand('vscode.openFolder', uri);

命令

vscode.executeWorkspaceSymbolProvider - 执行工作区所有的符号供应器函数

  • query - 搜索关键词
  • (returns) - promise函数, 且参数为具有SymbolInformation和DocumentSymbol的实例数组.

vscode.executeDefinitionProvider - 执行所有的定义供应器函数

  • uri - 文档的Uri
  • position - 某个符号的位置
  • (returns) - promise函数, 且参数为Location实例数组.

vscode.executeDeclarationProvider - 执行所有的声明供应器函数.

  • uri - 文档的Uri
  • position - 某个符号的位置
  • (returns) - promise函数, 且参数为Location实例数组.

vscode.executeTypeDefinitionProvider - 执行所有的类型定义供应器函数.

  • uri - 文档的Uri
  • position - 某个符号的位置
  • (returns) - promise函数, 且参数为Location实例数组.

vscode.executeImplementationProvider - 执行所有的接口供应器函数

  • uri - 文档的Uri
  • position - 某个符号的位置
  • (returns) - promise函数, 且参数为Location实例数组

vscode.executeHoverProvider - 执行所有的悬停供应器函数.

  • uri - 文档的Uri
  • position - 某个符号的位置
  • (returns) - promise函数, 且参数为Hover实例数组

vscode.executeDocumentHighlights - 执行文档高亮供应器函数.

  • uri - 文档的Uri
  • position - 在文档中的位置
  • (returns) - promise函数, 且参数为DocumentHighlight实例数组

vscode.executeReferenceProvider - 执行引用供应器函数

  • uri - 文档的Uri
  • position - 在文档中的位置
  • (returns) - promise函数, 且参数为Location实例数组

vscode.executeDocumentRenameProvider - 执行重命名供应器函数

  • uri - 文档的Uri
  • position - 在文档中的位置
  • newName - 新的符号名称
  • (returns) - promise函数, 且参数为WorkspaceEdit

vscode.executeSignatureHelpProvider - 执行符号帮助供应器函数

  • uri - 文档的Uri
  • position - 在文档中的位置
  • triggerCharacter - (可选的)当用户输入特定字符时(如,()触发符号帮助
  • (returns) - promise函数, 且参数为SignatureHelp

vscode.executeDocumentSymbolProvider - 执行文档符号供应器函数

  • uri - 文档的Uri
  • (returns) - promise函数, 且参数为具有SymbolInformation和DocumentSymbol的实例数组

vscode.executeCompletionItemProvider - 执行自动补全供应器函数

  • uri - 文档的Uri
  • position - 在文档中的位置
  • triggerCharacter - (可选的)当用户输入诸如(, ()之类的字符时触发
  • itemResolveCount - (可选的)补全的符号数量(数目太大会减慢补全速度)
  • (returns) - promise函数, 且参数为CompletionList实例

vscode.executeCodeActionProvider - 执行代码操作小灯泡提示供应器函数

  • uri - 文档的Uri
  • range - 在文档中的范围
  • (returns) - promise函数, 且参数为Command实例数组

vscode.executeCodeLensProvider - 执行CodeLens供应器函数

  • uri - 文档的Uri
  • itemResolveCount - (可选的)需要解析的lenses数量, 数目太大会影响性能
  • (returns) - promise函数, 且参数为CodeLens实例数组

vscode.executeFormatDocumentProvider - 执行格式化文档供应器函数

  • uri - 文档的Uri
  • options - 配置项
  • (returns) - promise函数, 且参数为TextEdits数组

vscode.executeFormatRangeProvider - 执行局部格式化供应器函数

  • uri - 文档的Uri
  • range - 限制的范围
  • options - 配置项
  • (returns) - promise函数, 且参数为TextEdits数组

vscode.executeFormatOnTypeProvider - 执行格式化文档供应器函数

  • uri - 文档的Uri
  • position - 在文档中的位置
  • ch - 在输入某个字符之后进行格式化
  • options - 配置项
  • (returns) - promise函数, 且参数为TextEdits数组

vscode.executeLinkProvider - 执行文档链接供应器函数

  • uri - 文档的Uri
  • (returns) - promise函数, 且参数为DocumentLink实例数组

vscode.executeDocumentColorProvider - 执行文档颜色供应器函数

  • uri - 文档的Uri
  • (returns) - promise函数, 且参数为ColorInfomation对象数组

vscode.executeColorPresentationProvider - 执行色彩呈现供应器函数

  • color - 需要展示并插入的颜色
  • context - 上下文对象, 包括uri和影响范围
  • (returns) - promise函数, 且参数为ColorPresentation对象数组

vscode.openFolder - 在当前窗口或者新的窗口打开一个文件夹或者工作区

  • uri - 被打开的文件夹或工作区Uri. 如果未提供, 会打开一个询问提示框
  • newWindow - (可选的)是否在新窗口打开. 默认在本窗口

!> 注意: 在当前窗口打开, 如果未设置newWindow = true, 会在指定的工作区或者文件夹开启新的拓展主机进程, 并且关闭当前拓展主机进程.

vscode.diff - 在diff编辑器中打开指定资源以比较它们的内容

  • left diff编辑器左边的文件
  • right diff编辑器右边的文件
  • title (可选)diff编辑器标题
  • options (可选)编辑器配置项, 参考vscode.TextDocumentShowOptions

vscode.open - 在编辑器打开指定文件

  • resource - 要打开的文件
  • columnOrOptions - (可选)可以是要打开的编辑器列,也可以是编辑器选项,参考vscode.TextDocumentShowOptions

可以是文本文件、二进制文件、http(s) url. 如果需要更多的配置项, 使用vscode.window.showTextDocument代替.

vscode.removeFromRecentlyOpened - 在最近打开的列表中移除一个路径

  • path - 被移除的路径

vscode.setEditorLayout - 设置编辑器布局

  • layout - 被设置的布局

布局是一个对象,带有初始布局方向(可选,0 = 水平布局,1 = 垂直布局),还有一个包含编辑器组的数组。每个编辑器组又有一个尺寸和另一个数组,其中有矩形布局和方向信息。如果设置了编辑器组的大小,每一行或者每一列的总和必须为1。比如一个2x2的网格:{ orientation: 0, groups: [{ groups: [{}, {}], size: 0.5 }, { groups: [{}, {}], size: 0.5 }] }

cursorMove - 移动光标到视图的合理位置

  • Cursor move argument object

    可以传递的键值对

    • ‘to’: 必选, 鼠标要移动到的合理位置

      'left', 'right', 'up', 'down'
      'wrappedLineStart', 'wrappedLineEnd', 'wrappedLineColumnCenter'
      'wrappedLineFirstNonWhitespaceCharacter', 'wrappedLineLastNonWhitespaceCharacter'
      'viewPortTop', 'viewPortCenter', 'viewPortBottom', 'viewPortIfOutside'
    • ‘by’: 移动的单位. 默认根据’to’来计算.

      'line', 'wrappedLine', 'character', 'halfLine'
    • ‘value’: 单位步数. 默认为’1’.

    • ‘select’: 如果为’true’则会选中. 默认为’false’.

editorScroll - 编辑器滚动方向

  • Editor scroll argument object

    可以传递的键值对

    • ‘to’: 必须的. 方向值

      'up', 'down'
    • ‘by’: 移动的单位. 默认根据’to’来计算.

      'line', 'wrappedLine', 'page', 'halfPage'
    • ‘value’: 单位步数. 默认为’1’.

    • ‘revealCursor’: 如果为’true’, 在超出滚动视图也会显示光标.

revealLine - 在给定的位置显示行

  • Reveal line argument object

    可以传递的键值对

    • ‘lineNumber’: 必须的. 行号

    • ‘at’: 显示的合理位置

      'top', 'center', 'bottom'

editor.unfold - 展开编辑器内容

  • Unfold editor argument

    可以传递的键值对

    • ‘levels’: 展开的层级数. 默认为 1.
    • ‘direction’: 如果是’up’, 向上展开, 否则向下展开
    • ‘selectionLines’: 要使用展开功能的起始行(从0起)。如果不设置,就会使用当前激活的行(选中区).

editor.fold - 折叠编辑器内容

  • Fold editor argument

    可以传递的键值对

    • ‘levels’: 折叠的的层级数。默认为1
    • ‘direction’: 如果设置为’up’,向上折叠,不然向下折叠
    • ‘selectionLines’: 要使用折叠功能的起始行(从0起)。如果不设置,就会使用当前激活的行(选中区)

editor.action.showReferences - 在文件中显示引用

  • uri - 要显示引用的文件
  • position - 要显示的位置
  • locations - 位置数组

moveActiveEditor - 通过标签或者组移动激活的编辑器

  • Active editor move argument

    参数

    • ‘to’: String. 目标位置
    • ‘by’: String. 移动的单位(通过标签或者组).
    • ‘value’: Number. 要移动的位置或者绝对位置值

简单命令

简单的命令不需要参数, 可以在keybindings.json键盘快捷方式列表中找到. 在文件底部的注释块中列出了未绑定的命令.

查看keybindings.json:

Windows, Linux: 文件 > 首选项 > 键盘快捷方式 > keybindings.json

macOS: 编码 > 首选项 > 键盘快捷方式 > keybindings.json

主题色彩

可以通过用户设置workbench.colorCustomizations配置项,定制Visual Studio Code的色彩主题

{
  "workbench.colorCustomizations": {
    "activityBar.background": "#00AA00"
  }
}

!> 注意: 如果你想用现成的颜色主题的话,通过首选项: 颜色主题的下拉菜单选取即可(Ctrl+K Ctrl+T),参阅色彩主题

颜色格式


可以使用RGBA来定义色值。同样,也支持十六进制表示法: #RGB#RGBA#RRGGBB#RRGGBBAA。R(红),G(绿),B(蓝),A(阿尔法)是一个十六进制字符(0-9,a-f,A-F)。#RRGGBB#RRGGBBAA分别可以简写为#RGB#RGBA。比如,#ee3355ff#e35f是一样的效果。

如果没有定义alpha值,那么它的默认值是ff(不透明,没有透明度)。反之,如果设为00,则完全透明。

一些颜色应该设置成透明的,以免遮挡其他注释的视线。查看颜色描述来了解哪些颜色应该使用这个规则。

对比色


一般用于高对比度的主题。通过给UI项添加额外的边框来增强对比度。

  • contrastActiveBorder: 在活动元素周围额外的一层边框,用来提高对比度从而区别其他元素
  • contrastBorder: 在元素周围额外的一层边框,用来提高对比度从而区别其他元素

基色


  • focusBorder: 焦点元素的整体边框颜色。此颜色仅在不被其他组件覆盖时适用
  • foreground: 整体前景色。此颜色仅在不被组件覆盖时适用
  • widget.shadow: 编辑器内小组件(如查找/替换)的阴影颜色
  • selection.background: 工作台所选文本的背景颜色(例如输入字段或文本区域)。注意,本设置不适用于编辑器
  • descriptionForeground: 提供其他信息的说明文本的前景色,例如标签文本
  • errorForeground: 错误信息的全局前景色。此颜色仅在不被组件覆盖时适用

文本颜色


文本文档中的颜色,比如欢迎页

  • textBlockQuote.background: 文本中块引用的背景颜色
  • textBlockQuote.border: 文本中块引用的边框颜色
  • textCodeBlock.background: 文本中代码块的背景颜色
  • textLink.activeForeground: 鼠标点击后或悬停时链接的前景色
  • textLink.foreground: 文本中链接的前景色
  • textPreformat.foreground: 预格式化文本段的前景色
  • textSeparator.foreground: 文字分隔符的颜色

按钮控件


按钮小组件的颜色,例如新窗口的资源管理器中的打开文件夹按钮。

  • button.background: 按钮背景色
  • button.foreground: 按钮前景色
  • button.hoverBackground: 鼠标悬停时按钮的背景颜色

下拉列表控件


下拉列表小部件颜色,例如集成终端和输出面板。

!> 注意: macOS目前还不能用该控件

  • dropdown.background: 下拉列表背景色
  • dropdown.listBackground: 下拉列表背景色
  • dropdown.border: 下拉列表边框
  • dropdown.foreground: 下拉列表前景色

输入框控件


输入框控件颜色,例如搜索视图、搜索/替换对话框。

  • input.background: 输入框背景色
  • input.border: 输入框边框
  • input.foreground: 输入框前景色
  • input.placeholderForeground: 输入框中占位符的前景色
  • inputOption.activeBorder: 输入字段中已激活选项的边框颜色
  • inputValidation.errorBackground: 输入验证结果为错误级别时的背景色
  • inputValidation.errorForeground: 输入验证结果为错误级别时的前景色
  • inputValidation.errorBorder: 严重性为错误时输入验证的边框颜色
  • inputValidation.infoBackground: 输入验证结果为信息级别时的背景色
  • inputValidation.infoForeground: 输入验证结果为信息级别时的前景色
  • inputValidation.infoBorder: 严重性为信息时输入验证的边框颜色
  • inputValidation.warningBackground: 严重性为警告时输入验证的背景色
  • inputValidation.warningForeground: 输入验证结果为警告级别时的前景色
  • inputValidation.warningBorder: 严重性为警告时输入验证的边框颜色

滚动条控件


  • scrollbar.shadow: 视图滚动后,滚动条的阴影
  • scrollbarSlider.activeBackground: 点击滚动条滑块后的背景色
  • scrollbarSlider.background: 滚动条滑块背景色
  • scrollbarSlider.hoverBackground: 鼠标悬停滚动条滑块时的背景色

徽章


Badge 是小型的信息标签,如表示搜索结果数量的标签

  • badge.foreground: Badge前景色
  • badge.background: Badge背景色

进度条


  • progressBar.background: 表示长时间操作的进度条的背景色

列表和树


列表和树的色彩,例如资源管理器。激活的列表/树具有键盘焦点,反之则没有。

  • list.activeSelectionBackground: 激活列表/树时已选项的背景色
  • list.activeSelectionForeground: 列表/树激活时已选项的前景色
  • list.dropBackground: 使用鼠标移动列表项时,列表/树的背景颜色
  • list.focusBackground: 列表/树激活时焦点项的背景色
  • list.focusForeground: 列表/树激活时焦点项的前景色
  • list.highlightForeground: 在列表或树中搜索时,其中匹配内容的高亮颜色
  • list.hoverBackground: 使用鼠标移动项目时,列表或树的背景颜色
  • list.hoverForeground: 鼠标在项目上悬停时,列表或树的前景颜色
  • list.inactiveSelectionBackground: 列表/树未激活时已选项的背景色
  • list.inactiveSelectionForeground: 列表/树未激活时已选项的前景色
  • list.inactiveFocusBackground: 非激活的列表或树控件中焦点项的背景颜色
  • list.invalidItemForeground: 列表或树中无效项的前景色,例如资源管理器中没有解析的根目录
  • list.errorForeground: 包含错误的列表项的前景颜色
  • list.warningForeground: 包含警告的列表项的前景颜色
  • listFilterWidget.background: 列表和树中类型筛选器小组件的背景色
  • listFilterWidget.outline: 列表和树中类型筛选器小组件的轮廓颜色
  • listFilterWidget.noMatchesOutline: 当没有匹配项时,列表和树中类型筛选器小组件的轮廓颜色

活动栏


活动栏可显示在最左侧或最右侧,供用户快速切换侧边栏视图

  • activityBar.background: 活动栏背景色
  • activityBar.dropBackground: 拖放活动栏项时的视觉反馈颜色。此颜色应有透明度,以便活动栏条目能透过此颜色
  • activityBar.foreground: 激活时活动栏项的前景色
  • activityBar.inactiveForeground: 未激活时活动栏项的前景色
  • activityBar.border: 活动栏分隔侧边栏的边框颜色
  • activityBarBadge.background: 活动通知徽章背景色
  • activityBarBadge.foreground: 活动通知徽章前景色

侧边栏


侧边栏是资源管理器和搜索等视图的容器。

  • sideBar.background: 侧边栏背景色
  • sideBar.foreground: 侧边栏前景色
  • sideBar.border: 侧边栏分隔编辑器的边框颜色
  • sideBar.dropBackground: 拖放侧边栏区域时的反馈颜色。此颜色应有透明度,以便侧边栏中的部分仍能透过
  • sideBarTitle.foreground: 侧边栏标题前景色
  • sideBarSectionHeader.background: 侧边栏节标题的背景颜色
  • sideBarSectionHeader.foreground: 侧边栏节标题的前景色
  • sideBarSectionHeader.border: 侧边栏节标题的边框颜色

编辑器组 & 选项卡


编辑器组是多个编辑器的容器,一个编辑器组可以包含多个编辑器。一个选项卡是一个编辑器的容器。可以在一个编辑器组里面打开多个选项卡。

  • editorGroup.border: 编辑器组之间的分隔颜色

  • editorGroup.dropBackground: 拖动编辑器时的背景颜色

  • editorGroupHeader.noTabsBackground: 禁用选项卡 ("workbench.editor.showTabs": false) 时编辑器组标题颜色

  • editorGroupHeader.tabsBackground: 启用选项卡时编辑器组标题的背景颜色

  • editorGroupHeader.tabsBorder: 选项卡启用时编辑器组标题的边框颜色。

  • editorGroup.emptyBackground: 空编辑器组的背景色
  • editorGroup.focusedEmptyBorder: 编辑器组被聚焦时的边框颜色
  • tab.activeBackground: 活动选项卡的背景色
  • tab.activeForeground: 活动组中活动选项卡的前景色
  • tab.border: 分隔多个选项卡的边框
  • tab.activeBorder: 活动选项卡底部的边框
  • tab.unfocusedActiveBorder: 在失去焦点的编辑器组中的活动选项卡底部的边框
  • tab.activeBorderTop: 活动选项卡顶部的边框
  • tab.unfocusedActiveBorderTop: 在失去焦点的编辑器组中的活动选项卡顶部的边框
  • tab.inactiveBackground: 非活动选项卡的背景色
  • tab.inactiveForeground: 活动组中非活动选项卡的前景色
  • tab.unfocusedActiveForeground: 一个失去焦点的编辑器组中的活动选项卡的前景色
  • tab.unfocusedInactiveForeground: 在一个失去焦点的组中非活动选项卡的前景色
  • tab.hoverBackground: 鼠标悬停时选项卡的背景色
  • tab.unfocusedHoverBackground: 鼠标悬停时非焦点组选项卡的背景色
  • tab.hoverBorder: 鼠标悬停时选项卡的边框颜色
  • tab.unfocusedHoverBorder: 鼠标悬停时非焦点组选项卡的边框颜色
  • tab.activeModifiedBorder : 在活动编辑器组中已修改 (存在更新) 的活动选项卡的顶部边框
  • tab.inactiveModifiedBorder: 在活动编辑器组中已修改 (存在更新) 的非活动选项卡的顶部边框
  • tab.unfocusedActiveModifiedBorder: 在未获焦点的编辑器组中已修改 (存在更新) 的活动选项卡的顶部边框
  • tab.unfocusedInactiveModifiedBorder: 在未获焦点的编辑器组中已修改 (存在更新) 的非活动选项卡的顶部边框
  • editorPane.background: 居中编辑器布局中左侧与右侧编辑器窗格的背景色

编辑器色彩


编辑器里面最重要的字符符号颜色主要是语法高亮。可以在色彩主题中或者使用editor.tokenColorCustomizations配置。参阅定制色彩主题以了解更新色彩主题和可用的标记类型

下面列出了所有的编辑器色彩:

  • editor.background: 编辑器背景色
  • editor.foreground: 编辑器前景色
  • editorLineNumber.foreground: 编辑器行号的颜色
  • editorLineNumber.activeForeground: 编辑器活动行号的颜色
  • editorCursor.background: 编辑器光标的背景色。可以自定义块型光标覆盖字符的颜色
  • editorCursor.foreground: 编辑器光标的前景色。可以自定义块型光标覆盖字符的颜色

当选中多个字符时会显示选区颜色。同时,与所选文本相关的区域也会高亮显示。

  • editor.selectionBackground: 所选内容的背景色
  • editor.selectionForeground: 所选文本的前景色
  • editor.inactiveSelectionBackground: 非活动编辑器中所选内容的颜色,颜色需带有透明度,以免遮挡底层样式
  • editor.selectionHighlightBackground: 具有与所选项相关内容的区域的颜色。颜色需带有透明度,以免遮挡底层样式
  • editor.selectionHighlightBorder: 与所选项内容相同的区域的边框颜色

当光标出现在符号或单词中时需显示单词高亮,依据语言插件的实现情况,可提供与高亮单词所对应声明和引用的语法高亮效果,但是这个高亮效果需和只读、书写的情况下的高亮效果有所区分。如果文档的语法插件不提供此项功能,那么高亮效果应降级到单纯的单词高亮:

  • editor.wordHighlightBackground: 读取访问期间符号的背景色,例如读取变量时。颜色需带有透明度,以免遮挡底层样式
  • editor.wordHighlightBorder: 符号在进行读取访问操作时的边框颜色,例如读取变量
  • editor.wordHighlightStrongBackground: 写入访问过程中符号的背景色,例如写入变量时。颜色需带有透明度,以免遮挡底层样式
  • editor.wordHighlightStrongBorder: 符号在进行写入访问操作时的边框颜色,例如写入变量

搜索匹配项的颜色取决于搜索/替换对话框中的输入文字:

  • editor.findMatchBackground: 当前搜索匹配项的颜色
  • editor.findMatchHighlightBackground: 其他搜索匹配项的颜色。颜色需带有透明度,以免遮挡底层样式
  • editor.findRangeHighlightBackground: 限制搜索范围的颜色(搜索弹出框小部件的‘在结果中查找’中可用)。颜色需带有透明度,以免遮挡底层样式
  • editor.findMatchBorder: 当前搜索匹配项的边框颜色
  • editor.findMatchHighlightBorder: 其他搜索匹配项的边框颜色
  • editor.findRangeHighlightBorder: 搜索范围限制中的边框颜色(搜索弹出框小部件的‘在结果中查找’中可用)

鼠标悬停时符号的颜色:

  • editor.hoverHighlightBackground: 在下面突出显示悬停的字词。颜色需带有透明度,以免遮挡底层样式

当前行(光标所在行)只会显示背景高亮或者边框高亮(两者之一)

  • editor.lineHighlightBackground: 光标所在行高亮内容的背景颜色
  • editor.lineHighlightBorder: 光标所在行四周边框的背景颜色

链接被点击时的颜色:

  • editorLink.activeForeground: 激活的链接的前景色

选择搜索结果时的范围高亮:

  • editor.rangeHighlightBackground: 限制搜索范围的颜色,用于快速打开、文件中的符号、搜索结果。颜色需带有透明度,以免遮挡底层样式
  • editor.rangeHighlightBorder: 限制搜索的范围的边框颜色

要查看编辑器在空白字符上显示符号的方式,启用(enable)Toggle Render Whitespace配置项。

  • editorWhitespace.foreground: 编辑器中空白字符的前景色

使用"editor.renderIndentGuides: true"配置编辑器显示缩进参考线

  • editorIndentGuide.background: 编辑器缩进参考线的颜色
  • editorIndentGuide.activeBackground: 编辑器活动缩进参考线的颜色

使用"editor.rulers"来配置编辑器标尺

  • editorRuler.foreground: 编辑器标尺的前景色

CodeLens:

  • editorCodeLens.foreground: 编辑器 CodeLens 的前景色

括号匹配:

  • editorBracketMatch.background: 匹配括号的背景色
  • editorBracketMatch.border: 匹配括号外框的颜色

缩略图标尺:

位于编辑器右边缘滚动条下方,可以概览整个编辑器。

  • editorOverviewRuler.border: 缩略图标尺边框的颜色
  • editorOverviewRuler.findMatchForeground: 用于查找匹配项的概述标尺标记颜色,颜色需带有透明度,以免遮挡底层样式
  • editorOverviewRuler.rangeHighlightForeground: 用于突出显示范围的概述标尺标记颜色,比如快速打开、文件中的符号、查找功能。颜色需带有透明度,以免遮挡底层样式
  • editorOverviewRuler.selectionHighlightForeground: 用于突出显示所选内容的概述标尺标记颜色。颜色需带有透明度,以免遮挡底层样式
  • editorOverviewRuler.wordHighlightForeground: 用于突出显示符号的概述标尺标记颜色。颜色需带有透明度,以免遮挡底层样式
  • editorOverviewRuler.wordHighlightStrongForeground: 用于突出显示写权限符号的概述标尺标记颜色。颜色需带有透明度,以免遮挡底层样式
  • editorOverviewRuler.modifiedForeground: 缩略图标尺中已修改内容的颜色
  • editorOverviewRuler.addedForeground: 缩略图标尺中已增加内容的颜色
  • editorOverviewRuler.deletedForeground: 缩略图标尺中已删除内容的颜色
  • editorOverviewRuler.errorForeground: 缩略图标尺中错误内容的颜色
  • editorOverviewRuler.warningForeground: 缩略图标尺中警告信息的颜色
  • editorOverviewRuler.infoForeground: 缩略图标尺中信息的颜色
  • editorOverviewRuler.bracketMatchForeground: 缩略图标尺上表示匹配括号的标记颜色

错误和警告:

  • editorError.foreground: 错误信息的整体前景色。此颜色仅在不被组件覆盖时适用
  • editorError.border: 编辑器中错误波浪线的边框颜色
  • editorWarning.foreground: 编辑器中警告波浪线的前景色
  • editorWarning.border: 编辑器中警告波浪线的边框颜色
  • editorInfo.foreground: 编辑器中信息波浪线的前景色
  • editorInfo.border: 编辑器中信息波浪线的边框颜色
  • editorHint.foreground: 编辑器中提示波浪线的前景色
  • editorHint.border: 编辑器中提示波浪线的边框颜色

未使用的源代码:

  • editorUnnecessaryCode.border: 编辑器中不必要(未使用)的源代码的边框颜色
  • editorUnnecessaryCode.opacity: 不必要(未使用)代码的在编辑器中显示的不透明度。例如,"#000000c0" 将以 75% 的不透明度显示代码。对于高对比度主题,请使用 "editorUnnecessaryCode.border" 主题来为非必须代码添加下划线,以避免颜色淡化

导航线包括字符边距和行号:

  • editorGutter.background: 编辑器导航线的背景色,导航线包括字符边距和行号
  • editorGutter.modifiedBackground: 编辑器导航线中被修改行的背景颜色
  • editorGutter.addedBackground: 编辑器导航线中已插入行的背景颜色
  • editorGutter.deletedBackground: 编辑器导航线中被删除行的背景颜色

差异编辑器色彩


已插入或者移除的文字的颜色,使用背景色或者边框色(两者选其一)

  • diffEditor.insertedTextBackground: 已插入的文本的背景色。颜色需带有透明度,以免遮挡底层样式
  • diffEditor.insertedTextBorder: 插入的文本的轮廓颜色
  • diffEditor.removedTextBackground: 已删除的文本的背景色。颜色需带有透明度,以免遮挡底层样式
  • diffEditor.removedTextBorder: 被删除文本的轮廓颜色
  • diffEditor.border: 两个文本编辑器之间的边框颜色

编辑器小部件色彩


编辑器组件在其内容的前面。例如(查找/替换)对话框、建议组件、编辑器悬浮提示框

  • editorWidget.background: 编辑器组件(如查找/替换)背景颜色
  • editorWidget.border: 编辑器小部件的边框颜色。此颜色仅在小部件有边框且不被小部件重写时适用
  • editorWidget.resizeBorder: 编辑器小部件大小调整条的边框颜色。此颜色仅在小部件有调整边框且不被小部件颜色覆盖时使用
  • editorSuggestWidget.background: 代码提示浮层的背景色
  • editorSuggestWidget.border: 代码提示浮层的边框颜色
  • editorSuggestWidget.foreground: 代码提示浮层的前景色
  • editorSuggestWidget.highlightForeground: 代码提示浮层中匹配内容的高亮颜色
  • editorSuggestWidget.selectedBackground: 代码提示浮层中所选条目的背景色
  • editorHoverWidget.background: 代码提示浮层背景颜色
  • editorHoverWidget.border: 代码提示浮层边框颜色

异常小组件是一个速览窗口,当调试抛出异常时出现

  • debugExceptionWidget.background: 异常小组件背景颜色
  • debugExceptionWidget.border: 异常小组件边框颜色

编辑器标记,当导航至编辑器中的错误和警告时出现(跳到下一个错误或警告命令)

  • editorMarkerNavigation.background: 编辑器标记导航小组件背景色
  • editorMarkerNavigationError.background: 编辑器标记导航小组件错误颜色
  • editorMarkerNavigationWarning.background: 编辑器标记导航小组件警告颜色
  • editorMarkerNavigationInfo.background: 编辑器标记导航小组件信息颜色

速览窗口色彩


速览窗口在编辑器内部,将引用和声明显示为视图

  • peekView.border: 速览视图边框和箭头颜色
  • peekViewEditor.background: 速览视图编辑器背景色
  • peekViewEditorGutter.background: 速览视图编辑器中装订线的背景色
  • peekViewEditor.matchHighlightBackground: 在速览视图编辑器中匹配突出显示颜色
  • peekViewEditor.matchHighlightBorder: 在速览视图编辑器中匹配项的突出显示边框
  • peekViewResult.background: 速览视图结果列表背景色
  • peekViewResult.fileForeground: 速览视图结果列表中文件节点的前景色
  • peekViewResult.lineForeground: 速览视图结果列表中行节点的前景色
  • peekViewResult.matchHighlightBackground: 在速览视图结果列表中匹配突出显示颜色
  • peekViewResult.selectionBackground: 速览视图结果列表中所选条目的背景色
  • peekViewResult.selectionForeground: 速览视图结果列表中所选条目的前景色
  • peekViewTitle.background: 速览视图标题区域背景颜色
  • peekViewTitleDescription.foreground: 速览视图标题信息颜色
  • peekViewTitleLabel.foreground: 速览视图标题颜色

合并冲突


合并冲突装饰,当编辑器包含范围差异时显示

  • merge.currentHeaderBackground: 当前标题的背景颜色。颜色需带有透明度,以免遮挡底层样式
  • merge.currentContentBackground: 当前内容背景色。颜色需带有透明度,以免遮挡底层样式
  • merge.incomingHeaderBackground: 传入标题背景色。颜色需带有透明度,以免遮挡底层样式
  • merge.incomingContentBackground: 传入内容背景色。颜色需带有透明度,以免遮挡底层样式
  • merge.border: 标头和分割线的边框颜色
  • merge.commonContentBackground: 共同祖先的内容背景色。颜色需带有透明度,以免遮挡底层样式
  • merge.commonHeaderBackground: 共同祖先的标头背景色,颜色需带有透明度,以免遮挡底层样式
  • editorOverviewRuler.currentContentForeground: 当前版本区域的缩略图标尺前景色
  • editorOverviewRuler.incomingContentForeground: 传入的版本区域的缩略图标尺前景色
  • editorOverviewRuler.commonContentForeground: 共同祖先内容的缩略图标尺前景色

面板色彩


面板显示在编辑器区域下方,包含输出和集成终端等视图

  • panel.background: 面板的背景色
  • panel.border: 将面板与编辑器隔开的边框的颜色
  • panel.dropBackground: 拖放面板标题项时的视觉反馈颜色,此颜色应有透明度,以免遮挡面板项
  • panelTitle.activeBorder: 活动面板标题的边框颜色
  • panelTitle.activeForeground: 活动面板的标题颜色
  • panelTitle.inactiveForeground: 非活动面板的标题颜色

状态栏色彩


状态栏显示在工作区底部

  • statusBar.background: 普通状态下,状态栏的背景色
  • statusBar.foreground: 普通状态下,状态栏的前景色
  • statusBar.border: 状态栏和编辑器间的分隔线颜色
  • statusBar.debuggingBackground: 调试程序时状态栏的背景色
  • statusBar.debuggingForeground: 调试程序时状态栏的前景色
  • statusBar.debuggingBorder: 调试程序时,状态栏和编辑器的分隔线颜色
  • statusBar.noFolderForeground: 没有打开文件夹时状态栏的前景色
  • statusBar.noFolderBackground: 没有打开文件夹时状态栏的背景色
  • statusBar.noFolderBorder: 没有打开文件夹时,状态栏和编辑器间的分隔线颜色
  • statusBarItem.activeBackground: 单击时的状态栏项背景色
  • statusBarItem.hoverBackground: 悬停时状态栏项背景色
  • statusBarItem.prominentBackground: 状态栏突出显示项的背景颜色,突出显示项比状态栏中的其他条目更醒目以表明其重要性,在命令面板中更改切换 Tab 键是否移动焦点可查看示例
  • statusBarItem.prominentHoverBackground: 鼠标悬停过程中状态栏突出显示项的背景颜色,突出显示项比状态栏中的其他条目更醒目以表明其重要性。在命令面板中更改切换 Tab 键是否移动焦点可查看示例

标题栏色彩


  • titleBar.activeBackground: 窗口处于活动状态时的标题栏背景色
  • titleBar.activeForeground: 窗口处于活动状态时的标题栏前景色
  • titleBar.inactiveBackground: 窗口处于非活动状态时的标题栏背景色
  • titleBar.inactiveForeground: 窗口处于非活动状态时的标题栏前景色
  • titleBar.border: 标题栏边框颜色

菜单栏色彩


  • menubar.selectionForeground: 菜单栏中选定菜单项的前景色
  • menubar.selectionBackground: 菜单栏中选定菜单项的背景色
  • menubar.selectionBorder: 菜单栏中所选菜单项的边框颜色
  • menu.foreground: 菜单项的前景颜色
  • menu.background: 菜单项的背景颜色
  • menu.selectionForeground: 菜单中选定菜单项的前景色
  • menu.selectionBackground: 菜单中所选菜单项的背景色
  • menu.selectionBorder: 菜单中所选菜单项的边框颜色
  • menu.separatorBackground: 菜单中分隔线的颜色

通知框色彩


!> 注意: 下列的色彩只适用于VS Code-1.21或更高版本

通知横幅从窗口右下角弹出

通知中心以带标题的列表显示

  • notificationCenter.border: 通知中心的边框颜色
  • notificationCenterHeader.foreground: 通知中心头部的前景色
  • notificationCenterHeader.background: 通知中心头部的背景色
  • notificationToast.border: 通知横幅的边框颜色
  • notifications.foreground: 通知的前景色
  • notifications.background: 通知的背景色
  • notifications.border: 通知中心中分隔通知的边框的颜色
  • notificationLink.foreground: 通知链接的前景色

如果你使用的VS Code版本低于1.21(2018-2),可以使用旧的色彩(不再支持):

  • notification.background
  • notification.foreground
  • notification.buttonBackground
  • notification.buttonForeground
  • notification.buttonHoverBackground
  • notification.errorBackground
  • notification.errorForeground
  • notification.infoBackground
  • notification.infoForeground
  • notification.warningBackground
  • notification.warningForeground

插件栏


  • extensionButton.prominentForeground: 扩展中突出操作的按钮前景色(比如 安装按钮)
  • extensionButton.prominentBackground: 扩展中突出操作的按钮背景色
  • extensionButton.prominentHoverBackground: 鼠标悬停时插件中突出操作的按钮的颜色

快速选取器


  • pickerGroup.border: 快速选取(快速打开)器分组边框的颜色
  • pickerGroup.foreground: 快速选取(快速打开)器分组标签的颜色

集成终端色彩


  • terminal.background: 终端视口的背景颜色
  • terminal.border: 分隔终端中拆分窗格的边框的颜色。默认为 panel.border 的颜色
  • terminal.foreground: 集成终端的默认前景色
  • terminal.ansiBlack: 终端中的’Black’ANSI
  • terminal.ansiBlue: 终端中的’Blue’ANSI
  • terminal.ansiBrightBlack: 终端中的’BrightBlack’ANSI
  • terminal.ansiBrightBlue: 终端中的’BrightBlue’ANSI
  • terminal.ansiBrightCyan: 终端中的’BrightCyan’ANSI
  • terminal.ansiBrightGreen: 终端中的’BrightGreen’ANSI
  • terminal.ansiBrightMagenta: 终端中的’BrightMagenta’ANSI
  • terminal.ansiBrightRed: 终端中的’BrightRed’ANSI
  • terminal.ansiBrightWhite: 终端中的’BrightWhite’ANSI
  • terminal.ansiBrightYellow: 终端中的’BrightYellow’ANSI
  • terminal.ansiCyan: 终端中的’Cyan’ANSI
  • terminal.ansiGreen: 终端中的’Green’ANSI
  • terminal.ansiMagenta: 终端中的’Magenta’ANSI
  • terminal.ansiRed: 终端中的’Red’ANSI
  • terminal.ansiWhite: 终端中的’White’ANSI
  • terminal.ansiYellow: 终端中的’Yellow’ANSI
  • terminal.selectionBackground: 终端中选中内容的背景色
  • terminalCursor.background: 终端光标的背景色。允许自定义被 block 光标遮住的字符的颜色
  • terminalCursor.foreground: 终端光标的前景色

调试


  • debugToolBar.background: 调试工具栏背景颜色
  • debugToolBar.border: 调试工具栏边框颜色
  • editor.stackFrameHighlightBackground: 堆栈帧中顶部一行的高亮背景色
  • editor.focusedStackFrameHighlightBackground: 堆栈帧中焦点一行的高亮背景色

欢迎界面


  • welcomePage.background: 欢迎页面的背景色
  • welcomePage.buttonBackground: 欢迎页按钮的背景色
  • welcomePage.buttonHoverBackground: 鼠标悬停时欢迎页按钮的背景色
  • walkThrough.embeddedEditorBackground: 嵌入于交互式操场中的编辑器的背景颜色

Git色彩


  • gitDecoration.addedResourceForeground: 新增的Git资源的前景色。用于显示文件标签和源代码管理
  • gitDecoration.modifiedResourceForeground: 修改过的Git资源的前景色。用于显示文件标签和源代码管理
  • gitDecoration.deletedResourceForeground: 移除过的Git资源的前景色。用于显示文件标签和源代码管理
  • gitDecoration.untrackedResourceForeground: 未跟踪的Git资源的前景色。用于文件标签和源代码管理
  • gitDecoration.ignoredResourceForeground: 已忽视的Git资源的前景色。用于文件标签和源代码管理
  • gitDecoration.conflictingResourceForeground: 冲突的Git资源的前景色。用于文件标签和源代码管理
  • gitDecoration.submoduleResourceForeground: 子模块资源的前景色

设置编辑器色彩


!> 注意: 下列色彩配置只适用于设置编辑器界面,可以通过首选项: 打开设置(UI)命令打开。

  • settings.headerForeground: 小节标题与活动标题的前景色
  • settings.modifiedItemIndicator: 已修改设置指示器的颜色
  • settings.dropdownBackground: 下拉列表的背景色
  • settings.dropdownForeground: 下拉列表的前景色
  • settings.dropdownBorder: 下拉列表的边框颜色
  • settings.dropdownListBorder: 下拉列表选项的边框颜色
  • settings.checkboxBackground: 复选框的背景色
  • settings.checkboxForeground: 复选框的前景色
  • settings.checkboxBorder: 复选框的边框颜色
  • settings.textInputBackground: 文本输入框的背景色
  • settings.textInputForeground: 文本输入框的前景色
  • settings.textInputBorder: 文本输入框的边框颜色
  • settings.numberInputBackground: 数字输入框的背景色
  • settings.numberInputForeground: 数字输入框的前景色
  • settings.numberInputBorder: 数字输入框的边框颜色

面包屑导航


面包屑导航的色彩主题:

  • breadcrumb.foreground: 导航路径的前景色
  • breadcrumb.background: 导航路径项的背景色
  • breadcrumb.focusForeground: 焦点导航路径的颜色
  • breadcrumb.activeSelectionForeground: 已选导航路径项的颜色
  • breadcrumbPicker.background: 导航路径项选择器的背景色

代码片段


代码片段的色彩主题:

  • editor.snippetTabstopHighlightBackground: 代码片段 Tab 位的高亮背景色
  • editor.snippetTabstopHighlightBorder: 代码片段 Tab 位的高亮边框颜色
  • editor.snippetFinalTabstopHighlightBackground: 代码片段中最后的 Tab 位的高亮背景色
  • editor.snippetFinalTabstopHighlightBorder: 代码片段中最后的 Tab 位的高亮边框颜色

也可以根据发布内容的颜色配置项,使用插件来发布色彩id(Ids)。当在workbench.colorCustomizations配置项中使用代码补全或者编辑色彩主题文件时,这些色彩也会出现。用户可以在插件发布选项卡中看到插件定义的色彩。

配置插件中的色彩


也可以根据发布内容的颜色配置项,使用插件来发布色彩id。当在workbench.colorCustomizations当编辑workbench.colorCustomizations和主题颜色文件时,这些色彩会出现在代码补全中。用户可以在插件发布选项卡中看到插件定义的色彩。

标签中的图标

你可以在插件中使用github提供的开源图标Octicons,你甚至可以在StatusBarItem文本和QuickPickItem标签中使用。添加图标的语法如下:

$(alert);

你还可以像这样使用多个标签

$(eye) $(heart) $(mark-github) GitHub

图标列表

图标列表请参考官方文档

文档选择器

插件的特性可以通过语言、类型、位置等文档选择器类型加以筛选,本节将深入文档选择器、文档协议等插件创作者应该了解的内容。

不在磁盘上的文件


并不是所有文件都是储存在磁盘上的,比如一份刚刚创建的文件。除非特别指明,文档选择器通常会应用于所有文档类型。使用DocumentFilterscheme属性将协议范围缩小,比如说,{ scheme: 'file', language: 'typescript' }是特定的用于储存在磁盘上的TypeScript文件。

文档选择器


VS Code插件API结合了特定的语言特性, 通过文档选择器的DocumentSelector类型, 可以支持例如智能感知(IntelliSense)等特性. 这是实现特定语言所支持特性的最为简单的机制.

下面的片段注册了一个Typescript文件的HoverProvider, 此时的文档选择器是typescript语言标识符.

vscode.languages.registerHoverProvider('typescript', {
  provideHover(doc: vscode.TextDocument) {
    return new vscode.Hover('For *all* TypeScript documents.');
  }
});

文档选择器可以不只是一个语言标识符, 还可以是复杂选择器——比如基于协议(scheme)和文件路径的DocumentFilter, 文件路径支持pattern参数和glob模式:

vscode.languages.registerHoverProvider(
  { pattern: '**/test/**' },
  {
    provideHover(doc: vscode.TextDocument) {
      return new vscode.Hover('For documents inside `test`-folders only');
    }
  }
);

下面这个片段, 使用合并后的协议(scheme)过滤器和语言标识符作为参数. 未命名的(untitled)协议正是为暂未保存到本地磁盘的文件准备的.

vscode.languages.registerHoverProvider(
  { scheme: 'untitled', language: 'typescript' },
  {
    provideHover(doc: vscode.TextDocument) {
      return new vscode.Hover('For new, unsaved TypeScript documents only');
    }
  }
);

文档协议


文档协议经常会被忽视, 但是它提供了很重要的信息. 插件开发者经常假设自己正在处理的文档也是存在磁盘上的. 用一个简单的typescript选择器做个例子, 假设Typescript文件在磁盘上, 不过大部分开发场景都过于宽松了,使用了诸如{ scheme: 'file', language: 'typescript' }显式的选择器。

当某项功能依赖于从磁盘上读/写文件时, 这个问题显得尤为重要. 请看下面的代码:

// 👎 too lax
vscode.languages.registerHoverProvider('typescript', {
  provideHover(doc: vscode.TextDocument) {
    const { size } = fs.statSync(doc.uri.fsPath); // ⚠️ what about 'untitled:/Untitled1.ts' or others?
    return new vscode.Hover(`Size in bytes is ${size}`);
  }
});

上面的例子中, 悬浮提示器想展示文件占用的磁盘大小, 但是它不会检查文档是不是真的存储在磁盘上. 比如, 一个新创建但是未保存的文件. 正确的做法是告诉VS Code只在文件存储在磁盘上时才开始工作.

// 👍 only works with files on disk
vscode.languages.registerHoverProvider(
  { scheme: 'file', language: 'typescript' },
  {
    provideHover(doc: vscode.TextDocument) {
      const { size } = fs.statSync(doc.uri.fsPath);
      return new vscode.Hover(`Size in bytes is ${size}`);
    }
  }
);

总结


文档通常都储存在文件系统中,但也有例外:未保存的新文件、Git使用的缓存文件、FTP上的远程文件等等。如果你的插件特性依赖于磁盘读取,那么你就要用文档选择器时应带上file协议。


文章作者: 杰克成
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 杰克成 !
评论
  目录