vscode插件开发实践

项目地址:https://github.com/CBDxin/cb-plugin
vscode插件市场搜索css-helper-plugin可试用

前置知识

vscode.languages.registerCompletionItemProvider

我们可以通过vscode.languages.registerCompletionItemProvider方法注册vscode的自动补全。

image.png

  • selector是要进行关联的文件类型;
  • provider是一个对象,里面必须包含provideCompletionItems和resolveCompletionItem这2个方法,provideCompletionItems用于定义自动补全的item,resolveCompletionItem用于定义光标选中当前自动补全item时触发动作,一般情况下无需处理;
  • triggerCharacters是一个可选的用于触发补全提示的字符列表;

vscode.languages.registerCodeLensProvider

codeLens可以在不影响代码结构的前提下在编译器中展示一些与代码相关的信息或者提供一些相关的操作,而我们平时用得最多的就是GitLens了。

image.png

通过vscode.languages.registerCodeLensProvider方法,我们可以定制一个属于自己的codeLens。

image.png

  • selector是要进行关联的文件类型;
  • provider是一个对象,里面必须包含provideCodeLenses和resolveCodeLenses这2个方法,provideCodeLenses用于定义lenses,返回一个lenses列表;

vscode.languages.registerHoverProvider

当我们把鼠标悬停在代码中的某个单词上面是,vscode有时会给予我们一些与这个单词相关的信息,


image.png

这是通过vscode.languages.registerHoverProvider实现的。

image.png

  • selector是要进行关联的文件类型;
  • HoverProvider用于定义悬停时产生的具体提示;

代码片段

在插件的package.json中添加如下配置:

"contributes": {
    "snippets": [
        {
            "language": "less",//snippets生效的语言类型
            "path": "./src/snippets/css.json"//snippets规则文件路径
        }
    ],
}

css.json文件内容如下:

{
  "cssAlias": {
    "prefix": "ca",
    "body": [
      "/*",
      "\talias:${0:alias}",
      "\tdesc:${1:desc}",
      "*/"
    ],
    "description": "Code snippet for 'setTimeout'"
  }
}

通过上面的配置,当我们在less文件中输入ca时,vscode可以为我们自动生成如下代码:


image.png

配置

我们可以定制插件的配置项,插件的配置项会出现在vscode的系统设置的扩展下面:


image.png

为了定制插件的配置,我们需要在package.json的属性中添加如下配置:

"configuration": {
    "type": "object",
    "title": "cb-pligin",
    "properties": {
        "cb-plugin.lessVariablesPath": {
        "type": "string",
        "default": "client\\common\\style\\variables.less",
        "description": "less变量文件路径"
    },
    "cb-plugin.globalCssPath": {
        "type": "string",
        "default": "client\\common\\style\\index.less",
        "description": "全局css样式文件路径"
    }
}

通过vscode.workspace.getConfiguration().getvscode.workspace.getConfiguration().update方法你可以获取和设置你的配置项。

className自动补全

有时候,我们提前定义好一些全局或者是各个组件的样式文件,然后再到jsx中去填写className。这时经常会出现一些令人十分痛苦的情况,例如忘记了定义好的className,或者是拼写错误导致样式不生效。如果当我们在jsx的className时能够vscode能够进行补全提示,那就真的是太好了!通过vscode.languages.registerCompletionItemProvider,我们就可以实现这样的功能。

定义provideCompletionItems

const classMatchReg = /className=["|']/;

function provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
  const start: vscode.Position = new vscode.Position(position.line, 0);
  const range: vscode.Range = new vscode.Range(start, position);
  const text: string = document.getText(range);

  const rawClasses = classMatchReg.test(text);
  if (!rawClasses) {
    return [];
  }

  const globalCssPath = path.join(vscode.workspace.workspaceFolders[0].uri._fsPath, vscode.workspace.getConfiguration().get('cb-plugin.globalCssPath'));
  const classNames = findCssClassNames(globalCssPath);

  return classNames.map((className) => {
    const completionItem = new vscode.CompletionItem(className, vscode.CompletionItemKind.Variable);
    completionItem.detail = className;
    return completionItem;
  });
}

首先通过正则/className=["|']/匹配当前是否在填写className,若匹配通过,则通过findCssClassNames方法获取全局样式文件中的全部className。

import * as fs from 'fs';

const CLSAANAME_REG =  /[\s]*\.([^:\s]+)[\s]*{/g;


export default function findClassNames(lessPath: string){
  const classNames:string[] = [];
  if(fs.existsSync(lessPath)){
    const content = fs.readFileSync(lessPath, 'utf-8');
    let matched;
    while((matched = CLSAANAME_REG.exec(content)) !== null){
      classNames.push(matched[1])
    }
  }

  return classNames;
}

然后将各个className作为vscode.CompletionItem返回。

通过别名补全className

但有时候,一些类名难以记住,我们需要通过别名识别某个类名,

const classMatchReg = /className=["|']/;

function provideCompletionItems(document: vscode.TextDocument, position: vscode.Position) {
  const start: vscode.Position = new vscode.Position(position.line, 0);
  const range: vscode.Range = new vscode.Range(start, position);
  const text: string = document.getText(range);

  const rawClasses = classMatchReg.test(text);
  if (!rawClasses) {
    return [];
  }

  const globalCssPath = path.join(vscode.workspace.workspaceFolders[0].uri._fsPath, vscode.workspace.getConfiguration().get('cb-plugin.globalCssPath'));
  const aliases = findCssAlias(globalCssPath);

  return Object.keys(aliases).map((alias) => {
    const aliasValue = aliases[alias].value;
    const aliasDesc = aliases[alias].desc;

    const completionItem = new vscode.CompletionItem(aliasValue, vscode.CompletionItemKind.Variable);

    completionItem.detail = aliasDesc;
    completionItem.filterText = `${aliasValue}: ${alias};`;

    return completionItem;
  });
}

通过findCssAlias从

/*
  alias:alias
  desc:desc
*/

查找出alias、desc和对应className,并生成对应的vscode.CompletionItem

less变量codeLens

有时候我们定义了一些全局的less变量,但由于种种原因,如项目成员不清楚已有这样的一堆变量或者对这些变量不熟悉,直接从交互稿上直接复制一些css代码等,导致定义好的变量没有被用到,这种情况我们可以通过codeLens在页面中给用户一些提示信息,并为用户提供点击使用变量替换原来的值的功能。


2020-09-21 10-08-17 00_00_00-00_00_30.gif

定义CodeLensProvider:

export class CodelensProvider implements vscode.CodeLensProvider {

    private codeLenses: vscode.CodeLens[] = [];
    private regex: RegExp;
    private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>();
    public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;

    constructor() {
        this.regex = /.:[\s]*([^:\s;]+)/g;

        vscode.workspace.onDidChangeConfiguration((_) => {
            this._onDidChangeCodeLenses.fire();
        });
    }

    public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.CodeLens[] | Thenable<vscode.CodeLens[]> {

        if (vscode.workspace.getConfiguration("codelens-sample").get("enableCodeLens", true)) {
            this.codeLenses = [];
            const regex = new RegExp(this.regex);
            const text = document.getText();
            let matches, matchedAlias;
            //@ts-ignore
            const lessVariablesPath = path.join(vscode.workspace.workspaceFolders[0].uri._fsPath, vscode.workspace.getConfiguration().get('cb-plugin.lessVariablesPath'));
            const lessVariables = Object.assign({}, findVariables(lessVariablesPath));
            while ((matches = regex.exec(text)) !== null ) {
                matchedAlias = matchLessVariable(lessVariables, matches[1])
                if(matchedAlias){
                    const line = document.lineAt(document.positionAt(matches.index).line);
                    const indexOf = line.text.indexOf(matches[1]);
                    const position = new vscode.Position(line.lineNumber, indexOf);
                    const range = document.getWordRangeAtPosition(position, new RegExp(/([^:\s;]+)/g));
                    if (range) {
                        this.codeLenses.push(new tipCodeLens(document.fileName, range, matchedAlias, matches[1]));
                    }
                }
            }
            return this.codeLenses;
        }
        return [];
    }

    public resolveCodeLens(codeLens: vscode.CodeLens, token: vscode.CancellationToken) {
        return null;
    }
}

通过/.:[\s]*([^:\s;]+)/g匹配文件中的各个css值,通过matchLessVariable方法检查各个css值是否已经被定义:

function matchLessVariable(lessVariables: any, targetValue: string){
    for (const key in lessVariables) {
        if(lessVariables[key].toLocaleLowerCase() === targetValue.toLocaleLowerCase()){
            return key;
        }
    }
}

若已定义,则通过tipCodeLens生成对应codelens:

import { CodeLens, Range } from "vscode";

export default class TipCodeLens extends CodeLens {
  constructor(
    fileName: string,
    range: Range,
    alias: string,
    value: string
  ) {
    super(range, {
      arguments: [alias, value, fileName, range],
      command: "cb-plugin.codelensAction",
      title: `${value} can be replaced by ${alias},click to replace`
    });
  }
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,240评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,328评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,182评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,121评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,135评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,093评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,013评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,854评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,295评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,513评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,678评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,398评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,989评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,636评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,801评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,657评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,558评论 2 352