Hello everyone, I am Han Cao 🌿, a grass code ape 🐒 who has been working for one and a half years. If you like my article, you can follow ➕ and give a thumbs up. Please grow up with me

Note: this article covers all phases of vscode from design to implementation, so it can be quite lengthy.

background

I’ve always been a code obsessive-compulsive, and I’ve written two previous articles on clean code

  • The rough and tumble of clean code and refactoring
  • “How to gracefully Not write comments 🌿” the long road that every engineer is constantly pursuing

In the last two articles, I focused more on how to design and write code logic to make code more legible. While good naming, good packaging, and so on can make code more readable, we often face a more “basic” problem: spelling errors.

Spelling mistakes may not seem like a big deal, but they can cause a lot of unexpected problems, such as:

  • Introduce unexpected and difficult to locatebug
  • The semantics of the code are unclear, and it is difficult for the successor developer to understand the semantics of the program through incorrect spelling
  • Correct and incorrect spellings are intertwined, which is equivalent to the existence of multiple words corresponding to a term, increasing the maintenance burden
  • .

However, since we are all flesh and blood, it is difficult to ensure that our Code output does not have any spelling errors, so the Code Spell Checker plugin has been downloaded 4,216,410 times. This plugin can be done in our coding process to correct our spelling mistakes in time, very convenient.

However, I found some spelling mistakes in the process of code review and many spelling mistakes in the process of checking the old code of the project. Therefore, as a code obsessive-compulsive, I tried to make all spelling mistakes in the project appear in front of me by some means.

Even without correcting, I would like to have a look at the spelling errors of the whole project and conduct statistical analysis of the data.

So I spent a day on the Tomb-sweeping Day holiday (actually more than… I’m just bragging) developed a vscode plugin from 0 to 1: Project Spell Checker to help me check and count all spelling errors in my Project.

The plugin is available in the vscode plugin store

In the next chapter, I will introduce the specific functions of this plug-in.

Function is introduced

Project-spell-checker is a VSCode plug-in with the ability to customize and complete the spell check for the entire project.

  • Customize the range of files scanned by the plug-in through configuration files
  • throughtree-viewDisplays suspected spelling errors under each file and prompts guesses of up to six correct words
  • throughweb-viewShows which files each suspected misspelling appears in

Note: both tree-view and web-view support quick file jump

The configuration file

Json configuration file spout -checker-config.json can be placed in.vscode or.project. Project-spell-checker Press TAB to automatically generate the configuration file template content:

  • excludedFloders: Indicates the name of the directory not to be scanned
  • includedFileSubfixes: Indicates the suffix of the file to be scanned
  • excludedFileNames: Indicates the name of the file not to be scanned
  • whiteList: word whitelist (supports arrays of strings as well as split strings)

view

There are two buttons on the plug-in:

  • Button 1: a button shaped like a graph for scanning and openingweb-view
  • Button 2: The refresh button is used to scan and opentree-view

tree-view

The tree view is displayed in the following dimensions: File -> Suspected misspellings

The tree structure format is basically as follows:

| - fileName - [suspected spelling mistake count] | - mistake1 ✓ - > spelling Suggestions OR (| - mistake2 ✓ - > spelling Suggestions OR (| - mistake3 ✓ - > spelling Suggestions OR (| - mistake4 ✓ - > spelling Suggestions OR: (Copy the code

You can click on the file name to jump

web-view

The display dimension of page view is:

Suspected misspelling -> file

The tree structure format is basically as follows:

    ----|- mistake1     - file-path1
                      - file-path2
                  
root----|- mistake2     - file-path3
                         
    ----|- mistake3     - file-path4
Copy the code

Since the above overview doesn’t necessarily give you an idea of what the tree looks like, we took a screenshot and showed it as follows:

You can click on the file name to jump

Teasing & the original idea

This is the list of requirements I planned before development:

In the future, I also want to add spell-check for file names and batch modify for suspected misspellings.

Plug-in development walkthrough

Various links

  • The official document: code.visualstudio.com/api
  • Various apis for vscode plug-ins: vscode-api-cn.js.org/modules/vsc…
  • I have written about the initialization and preparation of the project in my previous article and will not repeat it here: juejin.cn/post/696604…
  • Git address of this project: github.com/hancao97/pr…
  • Tree-view and web-view development reference [I think write good] : blog.csdn.net/weixin_4227…

Below my development guide to the main logic code ~

Utility methods

Gets the workspace root directory

Get the workspace root using the workspaceFolders property under vscode.workspace

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;
}
Copy the code

Obtaining Configuration Information

Json or.project/ spon-checker-config. json.

Note that this method will return the default configuration if the user has no configuration information.

The method is not very difficult, but maybe I have revised many times, write ideas

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) {
        returnbasicConfig; }}Copy the code

Gets the list of files to scan

Getting the list of files to scan is primarily a recursion, and if the subfile is a directory, the recursion continues.

Note that directories and files configured in the configuration file that are not scanned should be filtered out (and only files with user-specified suffixes should be recorded).

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;
}
Copy the code

Get spelling error information

This method actually does a lot of things:

  • Read the contents of each file returned by the previous method and scan for English words

It’s a little bit of a lexical analysis

  • Check for spelling errors and statistics

There are two dimensions of statistics: file -> list of suspected misspellings, and list of suspected misspellings -> list of files. Used for tree-view and web-view respectively

This method, on the other hand, is a little more complicated, and we’ll break it down into several parts.

Read the file and scan for words

The general logic is that both capital letters and non-Latin letters are used as word divisions.

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); }}}}Copy the code

Check the spelling

Here my spell check to use www.npmjs.com/package/sim… This package, there are many packages available for spell checking, but this is the only one that works well in the VS code-Extension environment after my attempts.

We can also go to see the source code, the package to achieve the idea and its simple

It does have a spellCheck method that returns a Boolean value, but I found some problems using it (it turned out that only Windows had problems during debugging, but after release it was no problem, which was embarrassing, whereas MAC never had problems).

My use of getSuggestions has several benefits:

  • I can take the contents of the dictionarylowerCaseAfter comparison
  • And by the way I can do something with the suggestion information

But the performance will be reduced (blame it on the Windows debugging process, well not my fault, I am also a master of shaking the pan)

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(a); 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('/');
Copy the code

A complete method

Here I’ve simply used a healthWordSet to make it more efficient to determine if a word is misspelled.

MistakeInfoMap and mistakeWordMap are the data structures used to store the two dimensional spelling error information mentioned above.

const getSpellingMistakeInfo =  (fileList, checkerConfig, rootPath) = > {
    let currentWord = ' ';
    const mistakeInfoMap = new Map(a);// use set or map to improve performance
    const healthWordSet = new Set([...checkerConfig.whiteListSet]);
    // use to record word => suggestions & files reflect
    const mistakeWordMap = new Map(a);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(a); 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 {
            constmistake = 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
    }
}
Copy the code

Vscode interaction

Code completion

Code completion is also a cliche

First, configure package.json as follows, language is the file in which the code complement takes effect.

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

The./snippets.json file corresponding to path contains the configuration information of the code snippet, which is as follows:

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

tree-view

In the reference link above, I refer to the more straightforward article I used to develop tree-View. The whole process is to implement two classes:

  • TreeViewProvider
  • TreeItemNode

The TreeViewProvider is also implemented:

  • getTreeItem
  • getChildren
  • initTreeView

In getChildren, I check whether element exists by checking whether it is the root. If it is the root, its children are the file name information. If element exists and has an info field, it is a file, and its children are the spelling error information in the file.

This method uses the TreeItemNode class to construct the node. Its parent TreeItem constructor takes the following arguments:

  • nodelabel
  • The default expansion state of a node
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');
            // Bind the click event
            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 }); }}Copy the code

web-view

The main call is the Window.createWebViewPane API.

My HTML code is returned using template strings via the getHtml method.

I used Echarts for the tree diagram.

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, Word-wrap: break-word! Important;" > <title>Document</title> </head> > <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/[email protected]/dist/echarts.min.js" > < / script > < script > const vscode = acquireVsCodeApi(); var chartDom = document.getElementById('main'); var myChart = echarts.init(chartDom); var option; const data =The ${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>
    `
}
Copy the code

Web-view communicates to vscode

Use acquireVsCodeApi in HTML with vscode.postMessage:

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

Vscode:

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

Open the specified file

Nothing special, just call the window.showTextDocument API

vscode.window.showTextDocument(vscode.Uri.file(info.path))
Copy the code

conclusion

I haven’t write the article, the main reason is that recently all into leetcode brush army, and began trying to make a video to share my ideas and some embarrassed to talk to the extreme, this extremely in the two pictures above is my design in order to do the video (style reference the boss hired straight ones billboards).

Can b station search “siege lion cold grass”, if the attention grateful can not help ☀️

Recall the original intention of being an engineer, in fact, is: to create beauty with technology, I think I have been doing it for nearly two years, in fact, I can try more things. In the future, you will see more diversified cold grass. When the time is ripe, I can also present my beautiful and ideal things to you in the way of technology. Please look forward to it.

Finally, a blessing to you and myself:

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

May we live our lives

May we live our lives

The aurora glows

Milky Way of stars

Mountains and rivers cascade

The waves rippling

All good things will come with us

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

Thank you for your support. My wechat id is Hancao97. See you next time at 🌺