开发者社区> 攻城狮寒草> 正文
阿里云
为了无法计算的价值
打开APP
阿里云APP内打开

「代码强迫症?」从0到1实现项目代码拼写检查 vscode 插件:project-spell-checker(二)

简介: 「代码强迫症?」从0到1实现项目代码拼写检查 vscode 插件:project-spell-checker(二)
+关注继续查看

插件开发攻略


image


各种链接



下面我的开发攻略以本次的逻辑代码为主~


工具方法

获取工作区根目录


先来个简单的,获取工作区根目录其实就是利用 vscode.workspace 下的 workspaceFolders 属性


const getRootPath = () => {
    const rootInfo = vscode.workspace.workspaceFolders[0];
    if(!rootInfo) {
        vscode.window.showInformationMessage('no suspected spelling mistakes!');
        return;
    } else {
        vscode.window.showInformationMessage('start checking suspected spelling mistakes...');
        vscode.window.showInformationMessage('This may take a long time. Please be patient~');
    }
    return rootInfo.uri.fsPath;
}


获取配置信息


其实就是读取 .vscode/spell-checker-config.json 或者 .project/spell-checker-config.json 的配置信息。


需要注意的是,如果用户没有配置信息我这个方法还是会返回一个默认的配置信息。

方法不是很难,但是可能我修改了很多次,写的有点子乱


const getCheckerConfig = (rootPath) => {
    const vscodeConfigPath = path.join(rootPath, '.vscode/spell-checker-config.json');
    const projectConfigPath = path.join(rootPath, '.project/spell-checker-config.json');
    const basicWhiteList = basicWhiteWords.split(',');
    const basicConfig = {
        excludedDirNameSet: new Set(["node_modules", ".git"]),
        includedFileSuffixSet: new Set(),
        excludedFileNameSet: new Set([".DS_Store"]),
        whiteListSet: new Set(basicWhiteList)
    }
    let configPath;
    // support config file in .vscode or .project
    if(fs.existsSync(vscodeConfigPath)) {
        configPath = vscodeConfigPath;
    } else if(fs.existsSync(projectConfigPath)) {
        configPath = projectConfigPath;
    } else {
        return basicConfig;
    }
    try {
        // avoid parse error
        const config = JSON.parse(fs.readFileSync(configPath, {
            encoding: 'utf-8'
        }));
        // because of word cannot include spec chars
        // so whiteList support word connected by ‘,’ or word array
        basicConfig.excludedDirNameSet = config.excludedFloders ? new Set(config.excludedFloders) : basicConfig.excludedDirNameSet;
        basicConfig.includedFileSuffixSet = config.includedFileSubfixes ? new Set(config.includedFileSubfixes) : basicConfig.includedFileSuffixSet;
        basicConfig.excludedFileNameSet = config.excludedFileNames ? new Set(config.excludedFileNames) : basicConfig.excludedFileNameSet;
        if(config.whiteList instanceof Array) {
            basicConfig.whiteListSet = config.whiteList ? new Set(basicWhiteList.concat(config.whiteList)) : basicConfig.whiteListSet;
        } else {
            basicConfig.whiteListSet = config.whiteList ? new Set(basicWhiteList.concat(config.whiteList.split(','))) : basicConfig.whiteListSet;
        }
        return basicConfig;
    } catch(err) {
        return basicConfig;
    }
}


获取需要扫描的文件列表


获取需要扫描的文件列表主要是一个递归,如果子文件是一个目录,则递归继续扫描。


这里需要注意的是配置文件中配置的不进行扫描的目录和文件要过滤掉(以及仅记录用户指定后缀名的文件


const _isDir = (path) => {
    const state = fs.statSync(path);
    return !state.isFile();
}
const getFileList = (dirPath, checkerConfig) => {
    let dirSubItems = fs.readdirSync(dirPath);
    const fileList = [];
    for (const item of dirSubItems) {
        const childPath = path.join(dirPath, item);
        if (_isDir(childPath) && !checkerConfig.excludedDirNameSet.has(item)) {
            fileList.push(...getFileList(childPath, checkerConfig));
        } else if (!_isDir(childPath) &&(checkerConfig.includedFileSuffixSet.size == 0 || checkerConfig.includedFileSuffixSet.has(path.extname(item))) && !checkerConfig.excludedFileNameSet.has(item)) {
            fileList.push(childPath);
        }
    }
    return fileList;
}


获取拼写错误信息


这个方法其实要做的事情比较多:


  • 读取上一个方法返回的每一个文件的文件内容,并扫描出一个个的英文单词

相当于一个小型的词法分析

  • 检查拼写错误并统计信息

这里统计信息为两个维度:文件 -> 疑似拼写错误列表,疑似拼写错误 -> 文件列表。

分别用于 tree-viewweb-view

相对应的,这个方法就稍微复杂一点,我们拆分成几部分来讲。


读取文件并扫描单词


大致逻辑就是大写字母和非拉丁字母都会作为单词的分割。


for (const file of fileList) {
        const content = fs.readFileSync(file, {
            encoding: 'utf-8'
        });
        for (const char of content) {
            if (/[a-z]/.test(char)) {
                currentWord += char;
            } else if (/[A-Z]/.test(char)) {
                if(/^[A-Z]+$/.test(currentWord)) {
                    currentWord += char;
                } else {
                    handleCurrentWord(file);
                    currentWord = char;
                }
            } else {
                if (currentWord) {
                    handleCurrentWord(file);
                }
            }
        }
    }


检查拼写


这里我的拼写检查使用了 www.npmjs.com/package/sim… 这个包,其实对于拼写检查功能而言,有很多包可以用,但是经过我的尝试只有这个在 vscode-extension 环境下运行的比较顺畅。


源码大家也可以去看看,这个包的实现思路及其简单


他其实有 spellCheck 方法返回布尔值,但是我在使用过程中发现了一些问题(后来发现只有 windows 在调试过程中会有问题,发布后使用就没有问题了,就很尴尬,而 mac 一直都没有问题)。


我这里用 getSuggestions 进行判断其实有几个好处:

  • 我可以把字典中的内容进行 lowerCase 后进行比较
  • 顺便我可以对建议信息进行一些处理


但是性能会有所下降(都怪 windows 调试过程有问题,嗯不是我的锅,我也是甩锅大师了)


const SpellChecker = require('simple-spellchecker');
const dictionaryGB = SpellChecker.getDictionarySync("en-GB", path.join(__dirname, '../dict'));  
const dictionaryUS = SpellChecker.getDictionarySync("en-US", path.join(__dirname, '../dict')); 
...
// it's not support windows, so change the check exe.
// by this way, we can change lowercase to compare
// if(dictionaryGB.spellCheck(word) || dictionaryUS.spellCheck(word)) {
const suggestionsGbAndUs = new Set();
dictionaryGB.getSuggestions(word, 5, 3).forEach(str => {
    if(!str.includes('\'')) {
        suggestionsGbAndUs.add(str.toLowerCase());
    }
})
dictionaryUS.getSuggestions(word, 5, 3).forEach(str => {
    if(!str.includes('\'')) {
        suggestionsGbAndUs.add(str.toLowerCase());
    }
})
if(suggestionsGbAndUs.has(word)) {
    healthWordSet.add(word);
    return;
}
suggestions = [...suggestionsGbAndUs].join('/');


完整方法


这里我简单使用了一个 healthWordSet 以提升判断单词是否拼写错误的执行效率。

mistakeInfoMapmistakeWordMap 就是上文提到用于存储的两个维度拼写错误信息的数据结构。


const getSpellingMistakeInfo =  (fileList, checkerConfig, rootPath) => {
    let currentWord = '';
    const mistakeInfoMap = new Map();
    // use set or map to improve performance
    const healthWordSet = new Set([...checkerConfig.whiteListSet]);
    // use to record word => suggestions & files reflect
    const mistakeWordMap = new Map();
    const handleCurrentWord = (file) => {
        const word = currentWord.toLowerCase();
        currentWord = '';
        let suggestions = '';
        if(word.length <= 1 || healthWordSet.has(word)) {
            return;
        } else if(mistakeWordMap.has(word)) {
            suggestions = mistakeWordMap.get(word).suggestions;
            mistakeWordMap.get(word).files.add(file.replace(rootPath, ''));
        } else {
            // it's not support windows, so change the check exe.
            // by this way, we can change lowercase to compare
            // if(dictionaryGB.spellCheck(word) || dictionaryUS.spellCheck(word)) {
            const suggestionsGbAndUs = new Set();
            dictionaryGB.getSuggestions(word, 5, 3).forEach(str => {
                if(!str.includes('\'')) {
                    suggestionsGbAndUs.add(str.toLowerCase());
                }
            })
            dictionaryUS.getSuggestions(word, 5, 3).forEach(str => {
                if(!str.includes('\'')) {
                    suggestionsGbAndUs.add(str.toLowerCase());
                }
            })
            if(suggestionsGbAndUs.has(word)) {
                healthWordSet.add(word);
                return;
            }
            suggestions = [...suggestionsGbAndUs].join('/');
            mistakeWordMap.set(word, {suggestions, files: new Set([file.replace(rootPath, '')])});
        }
        const getBasicMistake = (word) => ({
            count: 1,
            word: new Map([[word, suggestions]])
        })
        if(!mistakeInfoMap.has(file)) {
            mistakeInfoMap.set(file, getBasicMistake(word));
        } else {
            const mistake = mistakeInfoMap.get(file);
            mistake.count++;
            mistake.word.set(word, suggestions);
        }
    };
    for (const file of fileList) {
        const content = fs.readFileSync(file, {
            encoding: 'utf-8'
        });
        for (const char of content) {
            if (/[a-z]/.test(char)) {
                currentWord += char;
            } else if (/[A-Z]/.test(char)) {
                if(/^[A-Z]+$/.test(currentWord)) {
                    currentWord += char;
                } else {
                    handleCurrentWord(file);
                    currentWord = char;
                }
            } else {
                if (currentWord) {
                    handleCurrentWord(file);
                }
            }
        }
    }
    const spellingMistakeInfo = [...mistakeInfoMap].map(item => ({
        name: path.basename(item[0]),
        path: item[0],
        info: {
            path: item[0],
            count: item[1].count,
            word: [...item[1].word].map(item => ({
                original: item[0],
                suggestion: item[1]
            }))
        }
    }))
    const mistakeWordInfo = [...mistakeWordMap].map(item => ({
        name: item[0],
        children: [...item[1].files].map(child => ({
            name: child,
            type: 'path'
        }))
    }))
    return {
        spellingMistakeInfo,
        mistakeWordInfo
    }
}


vscode 交互

代码补全


代码补全也是一个老生常谈的功能了~


首先在 package.json 进行如下配置,language 就是配置在什么文件中代码补全会生效。


"contributes": {
    "snippets": [
        {
            "language": "json",
            "path": "./snippets.json"
        }
    ],
}


path 对应的 ./snippets.json 中有代码段的配置信息,文件内容如下:


{
    "project-spell-checker:Configs": {
        "prefix": "project-spell-checker",
        "body": [
            "{",
            "   \"excludedFloders\": [\"node_modules\", \".git\"],",
            "   \"includedFileSubfixes\": [],",
            "   \"excludedFileNames\": [\".DS_Store\"],",
            "   \"whiteList\": \"string,or,array\"",
            "}"
        ],
        "description": "project-spell-checker:Configs"
    }
}


tree-view


上面的参考链接中有我开发 tree-view 参考过的比较通俗易懂的文章,整个流程其实就是实现两个类:

  • TreeViewProvider
  • TreeItemNode

TreeViewProvider 也是要去实现:

  • getTreeItem
  • getChildren
  • initTreeView【静态方法】


getChildren 中我判断 element 是否存在就是判断其是否为根结点,如果是根结点,那它的子结点就是文件名信息,如果 element 存在并有 info 字段就代码该结点是文件,则其子结点为该文件下的拼写错误信息。


该方法中使用了 TreeItemNode 类构造节点,他的父类 TreeItem 构造函数的参数为:

  • 节点 label
  • 节点的默认展开状态


const { TreeItem, window, TreeItemCollapsibleState, Uri } = require('vscode');
const path = require('path');
class TreeItemNode extends TreeItem {
    constructor(label, collapsibleState, info) {
        super(label, collapsibleState);
        this.info = info;
        if(!info) {
            this.iconPath = TreeItemNode.getIconUri('error');
        } else {
            this.iconPath = TreeItemNode.getIconUri('jump');
            // 绑定点击事件
            this.command = {
                title: String(this.label),
                command: 'itemClick', 
                tooltip: String(this.label),  
                arguments: [  
                    this.info,   
                ]
            }
        }
    }
    static getIconUri(name) {
        return Uri.file(path.join(__filename,'..', '..' ,`resources/${name}.svg`));
    }
}
class TreeViewProvider {
    constructor(tree) {
        this.tree = tree;
    }
    getTreeItem(element) {
        return element;
    }
    getChildren(element) {
        if(!element) {
            return this.tree.map(item => new TreeItemNode(`${item.name}-[${item.info.count} suspected]`, TreeItemCollapsibleState['Expanded'], item.info));
        } else if(element.info) {
            return element.info.word.map(item => new TreeItemNode(`${item.original} -✓-> ${item.suggestion || ':('}`, TreeItemCollapsibleState['None']))
        }
    }
    static initTreeView(tree) {
        const treeViewProvider = new TreeViewProvider(tree);
        window.createTreeView('spellCheckerTree-main', {
            treeDataProvider: treeViewProvider
        });
    }
}


web-view


主要调用 window.createWebviewPane api。

我的 html 代码通过 getHtml 方法使用模版字符串返回。

树形图我使用了 echarts


const { window, Uri } = require('vscode');
let webviewPanel;
function createWebView(context, viewColumn, data, rootPath) {
    if (webviewPanel === undefined) {
        webviewPanel = window.createWebviewPanel(
            'spelling-check-statistics',
            'spelling-check-statistics',
            viewColumn,
            {
                retainContextWhenHidden: true,
                enableScripts: true
            }
        )
    } else {
        webviewPanel.reveal();
    }
    webviewPanel.webview.html = getHtml(data);
    webviewPanel.onDidDispose(() => {
        webviewPanel = undefined;
    });
    return webviewPanel;
}
function getHtml(data) {
    const _data = {
        name: 'suspected mistakes',
        children: data
    }
    const _height = data.reduce((total, current) => {
        return total + current.children.length * 25;
    }, 0)
    return `
    <!DOCTYPE html>
    <html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Document</title>
    </head>
    
    <body style="background: #fff;">
        <div id="test"></div>
        <div style="width:100%;height:100vh;overflow: auto;">
            <div id="main" style="min-width: 100%;height: ${_height}px;"></div>
        </div>
        <script src="https://cdn.jsdelivr.net/npm/echarts@5.3.2/dist/echarts.min.js"></script>
        <script>
            const vscode = acquireVsCodeApi();
            var chartDom = document.getElementById('main');
            var myChart = echarts.init(chartDom);
            var option;
            const data = ${JSON.stringify(_data)};
            option = {
                tooltip: {
                  trigger: 'item',
                  triggerOn: 'mousemove',
                  formatter: '{b}'
                },
                series: [
                  {
                    type: 'tree',
                    data: [data],
                    top: '1%',
                    left: '15%',
                    bottom: '1%',
                    right: '60%',
                    symbolSize: 7,
                    initialTreeDepth: 1,
                    label: {
                        backgroundColor: '#fff',
                        position: 'left',
                        verticalAlign: 'middle',
                        align: 'right',
                        fontSize: 16
                    },
                    leaves: {
                      label: {
                        position: 'right',
                        verticalAlign: 'middle',
                        align: 'left'
                      }
                    },
                    emphasis: {
                      focus: 'descendant'
                    },
                    expandAndCollapse: true,
                    animationDuration: 550,
                    animationDurationUpdate: 750
                  }
                ]
            };
            option && myChart.setOption(option);
        </script>
    </body>
    </html>
    `
}


web-view 向 vscode 通信


html 中使用 acquireVsCodeApivscode.postMessage


const vscode = acquireVsCodeApi();
myChart.on('click', 'series', function (params) {
    if(params.data.type == 'path') {
        vscode.postMessage({jump: params.data.name});
    }
});


vscode 中进行监听:


webviewPanel.webview.onDidReceiveMessage(message => {
    //...
}, undefined, context.subscriptions);


打开指定文件


没啥特别的,就是调用 window.showTextDocument api


vscode.window.showTextDocument(vscode.Uri.file(info.path))


结束语


image


我已经很久没有写文章了,主要原因是最近一股脑投入到了 leetcode 刷题大军,以及开始尝试去创作视频来分享我的奇思妙想和一些尬到极致的脱口秀,上面的这个极其中二的图片也是我为了做视频设计的(样式参考了boss直聘的魔性广告牌)。


可以 b 站搜索“攻城狮寒草”,如果关注感激不禁☀️


回忆做一名工程师的初心,其实还是:用技术创造美好,我想我也做了快要两年了,其实也可以去尝试更多的东西了。未来大家会看到更加多元的寒草,待到时机成熟时,我也可以把自己心中美好的,理想的东西以技术方式呈现给大家,敬请期待吧。

最后给大家以及我自己一份祝福:


✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

愿我们可以活出自我
愿我们能够不负一生

极光斑斓
星河灿烂
山川层峦
水波荡漾

美好终会与我们相伴

✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

相关文章
Docker使用篇之容器数据卷(轻松实现项目部署到tomcat上)
Docker使用篇之容器数据卷(轻松实现项目部署到tomcat上)
53 0
SAP 电商云 Spartacus UI 同 SAP Customer Data Cloud 的集成
SAP 电商云 Spartacus UI 同 SAP Customer Data Cloud 的集成
42 0
SAP Commerce Cloud 的代码仓库
SAP Commerce Cloud 的代码仓库
14 0
SAP Cloud for Customer使用postman更新user profile的web service
SAP Cloud for Customer使用postman更新user profile的web service
29 0
SAP Hybris WCMS cockpit 的登录 url
SAP Hybris WCMS cockpit 的登录 url
27 0
running Extension project directly on ABAP server without Launchpad
Created by Jerry Wang, last modified on Sep 03, 2015
71 0
+关注
攻城狮寒草
我是寒草,一只工作一年半的草系码猿🐒。 间歇性热血🔥,持续性沙雕🌟,草系码猿期待着大家的点赞👍与关注➕。 [微信:hancao97]
68
文章
0
问答
文章排行榜
最热
最新
相关电子书
更多
低代码开发师(初级)实战教程
立即下载
阿里巴巴DevOps 最佳实践手册
立即下载
冬季实战营第三期:MySQL数据库进阶实战
立即下载