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


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

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


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

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

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

美好终会与我们相伴

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

相关文章
|
2月前
|
iOS开发 MacOS
【Mac系统】解决Vscode中LeetCode插件不能刷剑指offer题库
文章讨论了解决Mac系统中Vscode里LeetCode插件无法刷剑指Offer题库的问题,并提供了一些相关的使用技巧和资源链接。
134 1
|
29天前
|
JSON JavaScript 小程序
使用VSCode搭建UniApp + TS + Vue3 + Vite项目
`uniapp` 是一个基于 Vue.js 的框架,支持一次开发多端部署,深受前端开发者喜爱。本文详细介绍如何使用 `VSCode` 搭建 `uniapp` 项目,包括安装 `node` 和 `pnpm`、创建项目、安装扩展组件、配置 `Json` 文件注释及安装相关插件。通过这些步骤,你可以高效地使用 `VSCode` 开发 `uniapp` 项目,并享受代码提示和自动补全功能,提高开发效率。
123 24
使用VSCode搭建UniApp + TS + Vue3 + Vite项目
|
16天前
|
开发框架 .NET C#
VSCode开发.net项目时调试无效
【9月更文挑战第22天】在使用 VSCode 开发 .NET 项目时遇到调试问题,可从项目配置、调试配置、调试器安装、运行环境、日志和错误信息等方面排查。确认项目类型及文件配置,检查 `launch.json` 文件及配置项,确保调试器扩展已安装并启用,验证 .NET 运行时版本和环境变量,查看 VSCode 输出窗口和项目日志文件,检查权限及代码错误。若问题仍未解决,可查阅官方文档或社区论坛。
|
2月前
|
前端开发 Go
vscode10大常用插件
本文介绍了前端开发中常用的工具及VSCode必备插件。推荐使用VSCode作为入门工具,并介绍了WebStorm和HBuilder等其他选项。VSCode插件包括:Open-In-Browser、live-server、Beautify、Code Runner、Image Preview、Path Intellisense、Turbo Console Log、css-auto-prefix、Bracket Pair Colorizer 和 Auto Rename Tag,这些插件能够显著提升开发效率和代码质量。此外,还提供了录制Gif图的工具GifCam。
80 5
vscode10大常用插件
|
26天前
|
人工智能 C++ 开发者
verilog vscode 与AI 插件
【9月更文挑战第11天】在Verilog开发中,使用Visual Studio Code(VS Code)结合AI插件能显著提升效率。VS Code提供强大的编辑功能,如语法高亮、自动补全和代码格式化;便捷的调试功能,支持多种调试器;以及丰富的插件生态。AI插件则可自动生成代码、优化现有代码、检测并修复错误,还能自动生成文档。常用插件包括Verilog AI Assistant和Verilog Language Server,可根据需求选择合适的工具组合,提高开发效率和代码质量。
|
2月前
|
前端开发 IDE 开发工具
OpenSumi问题之OpenSumi 对于 VS Code 插件生态要如何支持
OpenSumi问题之OpenSumi 对于 VS Code 插件生态要如何支持
|
2月前
|
Dart
Flutter笔记:手动配置VSCode中Dart代码自动格式化
Flutter笔记:手动配置VSCode中Dart代码自动格式化
192 5
|
2月前
|
Java 数据安全/隐私保护
VScode将代码提交到远程服务器、同时解决每次提交都要输入密码的问题(这里以gitee为例子)
这篇文章介绍了如何在VSCode中将代码提交到Gitee远程服务器,并提供了解决每次提交都需要输入密码问题的方法。
VScode将代码提交到远程服务器、同时解决每次提交都要输入密码的问题(这里以gitee为例子)
|
2月前
|
JavaScript 前端开发 开发者
【颠覆你的前端世界!】VSCode + ESLint + Prettier:一键拯救Vue代码于水深火热之中,打造极致编程体验之旅!
【8月更文挑战第9天】随着前端技术的发展,保持代码规范一致至关重要。本文介绍如何在VSCode中利用ESLint和Prettier检查并格式化Vue.js代码。ESLint检测代码错误,Prettier保证风格统一。首先需安装VSCode插件及Node.js包,然后配置ESLint和Prettier选项。在VSCode设置中启用保存时自动修复与格式化功能。配置完成后,VSCode将自动应用规则,提升编码效率和代码质量。
253 1
|
2月前
|
C# C++
【Azure Function】在VS Code中创建Function项目遇见 No .NET worker runtimes found
【Azure Function】在VS Code中创建Function项目遇见 No .NET worker runtimes found