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

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

插件开发攻略


网络异常,图片无法展示
|


各种链接



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


工具方法

获取工作区根目录


先来个简单的,获取工作区根目录其实就是利用 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))


结束语


网络异常,图片无法展示
|


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


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


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

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


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

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

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

美好终会与我们相伴

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

相关文章
|
14天前
|
iOS开发 MacOS
【Mac系统】解决Vscode中LeetCode插件不能刷剑指offer题库
文章讨论了解决Mac系统中Vscode里LeetCode插件无法刷剑指Offer题库的问题,并提供了一些相关的使用技巧和资源链接。
41 1
|
8天前
|
Dart
Flutter笔记:手动配置VSCode中Dart代码自动格式化
Flutter笔记:手动配置VSCode中Dart代码自动格式化
34 5
|
5天前
|
Java 数据安全/隐私保护
VScode将代码提交到远程服务器、同时解决每次提交都要输入密码的问题(这里以gitee为例子)
这篇文章介绍了如何在VSCode中将代码提交到Gitee远程服务器,并提供了解决每次提交都需要输入密码问题的方法。
VScode将代码提交到远程服务器、同时解决每次提交都要输入密码的问题(这里以gitee为例子)
|
11天前
|
JavaScript 前端开发 开发者
【颠覆你的前端世界!】VSCode + ESLint + Prettier:一键拯救Vue代码于水深火热之中,打造极致编程体验之旅!
【8月更文挑战第9天】随着前端技术的发展,保持代码规范一致至关重要。本文介绍如何在VSCode中利用ESLint和Prettier检查并格式化Vue.js代码。ESLint检测代码错误,Prettier保证风格统一。首先需安装VSCode插件及Node.js包,然后配置ESLint和Prettier选项。在VSCode设置中启用保存时自动修复与格式化功能。配置完成后,VSCode将自动应用规则,提升编码效率和代码质量。
52 1
|
7天前
vscode——Prettier插件保存自动格式化
vscode——Prettier插件保存自动格式化
13 0
|
15天前
|
JavaScript Linux iOS开发
【Mac系统】Vscode使用LeetCode插件报错‘leetcode.toggleLeetCodeCn‘ not found
在Mac系统下使用VSCode的LeetCode插件时遇到“leetcode.toggleleetcodecn”命令找不到的错误解决方法,主要是通过从Nodejs官网下载并安装最新版本的Node.js来解决环境配置问题。
34 0
|
1月前
|
JavaScript IDE 开发工具
vue3【2024版】开发环境搭建(含官网和nvm下载切换最新版node,修改node下载源,创建项目,启动项目,安装vscode插件Vue - Official)
vue3【2024版】开发环境搭建(含官网和nvm下载切换最新版node,修改node下载源,创建项目,启动项目,安装vscode插件Vue - Official)
85 3
|
1月前
vscode 生成项目目录结构 directory-tree 实用教程
vscode 生成项目目录结构 directory-tree 实用教程
93 2
|
1月前
|
JavaScript 安全
下载安装 vscode(含汉化、插件的推荐和安装)
下载安装 vscode(含汉化、插件的推荐和安装)
34 0
下载安装 vscode(含汉化、插件的推荐和安装)
|
1月前
|
人工智能 JavaScript 开发工具
【完全免费】VS Code 最好用的 12 款 AI 代码提示插件!!!
🎉 探索12款免费VSCode AI代码提示插件:Codeium、Codegeex、CodeFuse、TONGYI Lingma、Comate、iFlyCode、Fitten Code、Bito AI、Mintlify Doc Writer、Kodezi AI、aiXcoder、IntelliCode。这些插件提供智能补全、代码生成、注释、优化,支持多种语言,提升编程效率!🚀👩‍💻👨‍💻
1586 0