「代码强迫症?」从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 站搜索“攻城狮寒草”,如果关注感激不禁☀️


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

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


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

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

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

美好终会与我们相伴

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

相关文章
|
27天前
|
JavaScript 前端开发 开发者
如何在 Visual Studio Code (VSCode) 中使用 ESLint 和 Prettier 来检查代码规范并自动格式化 Vue.js 代码。
【10月更文挑战第7天】随着前端开发技术的快速发展,代码规范和格式化工具变得尤为重要。本文介绍了如何在 Visual Studio Code (VSCode) 中使用 ESLint 和 Prettier 来检查代码规范并自动格式化 Vue.js 代码。通过安装和配置这两个工具,可以确保代码风格一致,提升团队协作效率和代码质量。
195 2
|
9天前
|
JavaScript 前端开发 开发者
如何在 Visual Studio Code (VSCode) 中使用 ESLint 和 Prettier 检查代码规范并自动格式化 Vue.js 代码,包括安装插件、配置 ESLint 和 Prettier 以及 VSCode 设置的具体步骤
随着前端开发技术的快速发展,代码规范和格式化工具变得尤为重要。本文介绍了如何在 Visual Studio Code (VSCode) 中使用 ESLint 和 Prettier 检查代码规范并自动格式化 Vue.js 代码,包括安装插件、配置 ESLint 和 Prettier 以及 VSCode 设置的具体步骤。通过这些工具,可以显著提升编码效率和代码质量。
110 4
|
7天前
|
JavaScript 前端开发 开发者
如何在 Visual Studio Code (VSCode) 中使用 ESLint 和 Prettier 检查代码规范并自动格式化 Vue.js 代码
随着前端开发技术的快速发展,代码规范和格式化工具变得尤为重要。本文介绍如何在 Visual Studio Code (VSCode) 中使用 ESLint 和 Prettier 检查代码规范并自动格式化 Vue.js 代码。通过安装和配置这些工具,可以确保代码风格一致,提高代码质量和可读性。
22 1
|
14天前
|
前端开发 JavaScript 数据库
VSCode编程助手工程能力体验报告(一):通义灵码 - 帮你高效切入新项目、编码和提升质量
我是一位软件工程师,用通义灵码个人版vscode插件的workspace做项目分析和复盘,对比之前没有灵码,现在提效了80%,本文介绍了具体的使用流程。
|
17天前
|
自然语言处理 JavaScript 开发者
通义灵码插件:VSCode 的智能编程助手
通义灵码插件:VSCode 的智能编程助手
131 3
|
2月前
|
JSON JavaScript 小程序
使用VSCode搭建UniApp + TS + Vue3 + Vite项目
`uniapp` 是一个基于 Vue.js 的框架,支持一次开发多端部署,深受前端开发者喜爱。本文详细介绍如何使用 `VSCode` 搭建 `uniapp` 项目,包括安装 `node` 和 `pnpm`、创建项目、安装扩展组件、配置 `Json` 文件注释及安装相关插件。通过这些步骤,你可以高效地使用 `VSCode` 开发 `uniapp` 项目,并享受代码提示和自动补全功能,提高开发效率。
248 24
使用VSCode搭建UniApp + TS + Vue3 + Vite项目
|
20天前
|
前端开发 JavaScript 编译器
2024最新VSCode实用插件推荐,开发效率遥遥领先!超全面,快收藏~
【10月更文挑战第11天】2024最新VSCode实用插件推荐,开发效率遥遥领先!超全面,快收藏~
42 0
2024最新VSCode实用插件推荐,开发效率遥遥领先!超全面,快收藏~
|
25天前
|
JavaScript 前端开发 开发者
如何在 VSCode 中使用 ESLint 和 Prettier 检查并自动格式化 Vue.js 代码,提升团队协作效率和代码质量。
【10月更文挑战第9天】随着前端开发技术的发展,代码规范和格式化工具变得至关重要。本文介绍如何在 VSCode 中使用 ESLint 和 Prettier 检查并自动格式化 Vue.js 代码,提升团队协作效率和代码质量。通过安装插件、配置 ESLint 和 Prettier,以及设置 VSCode,实现代码实时检查和格式化,确保代码风格一致。
20 2
|
26天前
|
Ubuntu Linux 编译器
Linux/Ubuntu下使用VS Code配置C/C++项目环境调用OpenCV
通过以上步骤,您已经成功在Ubuntu系统下的VS Code中配置了C/C++项目环境,并能够调用OpenCV库进行开发。请确保每一步都按照您的系统实际情况进行适当调整。
209 3
|
23天前
|
IDE 网络安全 开发工具
IDE之vscode:连接远程服务器代码(亲测OK),与pycharm链接服务器做对比(亲自使用过了),打开文件夹后切换文件夹。
本文介绍了如何使用VS Code通过Remote-SSH插件连接远程服务器进行代码开发,并与PyCharm进行了对比。作者认为VS Code在连接和配置多个服务器时更为简单,推荐使用VS Code。文章详细说明了VS Code的安装、远程插件安装、SSH配置文件编写、服务器连接以及如何在连接后切换文件夹。此外,还提供了使用密钥进行免密登录的方法和解决权限问题的步骤。
205 0
IDE之vscode:连接远程服务器代码(亲测OK),与pycharm链接服务器做对比(亲自使用过了),打开文件夹后切换文件夹。