Snippet, an extension for VSCode and CLI. Part 1





Good day, friends!



While developing the Modern HTML Starter Template, I thought about expanding its usability. At that time, the options for its use were limited to cloning the repository and downloading the archive. This is how the HTML snippet and extension for Microsoft Visual Studio Code - HTML Template , as well as the command line interface - create-modern-template appeared . Of course, these tools are far from perfect and I will refine them as much as I can. However, in the process of creating them, I learned a few interesting things that I want to share with you.



In this part we will look at the snippet and extension, and the CLI in the next.



If you are only interested in the source code, here is the link to the repository .



Snippet



What is a snippet? In short, a snippet is a template that the editor uses for autocomplete (code completion).



VSCode has built in Emmet ( official site , Emmet in Visual Studio Code ), which uses numerous HTML, CSS and JS snippets to help you write your code. We type in the editor (in .html)!, Press Tab or Enter, we get the finished html5 markup. We type nav> ul> li * 3> a.link> img, press Tab, we get:



<nav>
    <ul>
      <li><a href="" class="link"><img src="" alt=""></a></li>
      <li><a href="" class="link"><img src="" alt=""></a></li>
      <li><a href="" class="link"><img src="" alt=""></a></li>
    </ul>
  </nav>

      
      





etc.



In addition to the built-in ones, VSCode provides the ability to use custom snippets. To create them, go to File -> Preferences -> User Snippets (or click on the Manage button in the lower left corner and select User Snippets). The settings for each language are stored in a corresponding JSON file (for HTML in html.json, for JavaScript in javascript.json, etc.).



Let's practice creating JS snippets. Find the javascript.json file and open it.







We see comments briefly describing the rules for creating snippets. More information on creating custom snippets in VSCode can be found here .



Let's start with something simple. Let's create a snippet for console.log (). This is how it looks:



"Print to console": {
  "prefix": "log",
  "body": "console.log($0)",
  "description": "Create console.log()"
},

      
      





  • Print to console - object key, snippet name (required)
  • prefix - shorthand for snippet (required)
  • body - the snippet itself (required)
  • $ number - cursor position after snippet creation; $ 1 - first position, $ 2 - second, etc., $ 0 - last position (optional)
  • description - snippet description (optional)


We save the file. We type log in the script, press Tab or Enter, we get console.log () with the cursor between the brackets.



Let's create a snippet for the for-of loop:



"For-of loop": {
  "prefix": "fo",
  "body": [
    "for (const ${1:item} of ${2:arr}) {",
    "\t$0",
    "}"
  ]
},

      
      





  • Multi-line snippets are created using an array
  • $ {number: value}; $ {1: item} means first cursor position with default item value; this value is highlighted after creating a snippet, as well as after moving to the next position of the cursor for quick editing
  • \ t - one indent (the amount of space is determined by the corresponding editor settings or, in my case, the Prettier extension ), \ t \ t - two indents, etc.


We type fo in the script, press Tab or Enter, we get:



for (const item of arr) {

}

      
      





with item highlighted. Press Tab, arr is highlighted. Press Tab again, go to the second line.



Here are some more examples:



"For-in loop": {
  "prefix": "fi",
  "body": [
    "for (const ${1:key} in ${2:obj}) {",
    "\t$0",
    "}"
  ]
},
"Get one element": {
  "prefix": "qs",
  "body": "const $1 = ${2:document}.querySelector('$0')"
},
"Get all elements": {
  "prefix": "qsa",
  "body": "const $1 = [...${2:document}.querySelectorAll('$0')]"
},
"Add listener": {
  "prefix": "al",
  "body": [
    "${1:document}.addEventListener('${2:click}', (${3:{ target }}) => {",
    "\t$0",
    "})"
  ]
},
"Async function": {
  "prefix": "af",
  "body": [
    "const $1 = async ($2) => {",
    "\ttry {",
    "\t\tconst response = await fetch($3)",
    "\t\tconst data = await res.json()",
    "\t\t$0",
    "\t} catch (err) {",
    "\t\tconsole.error(err)",
    "\t}",
    "}"
  ]
}

      
      





HTML snippets follow the same principle. This is what the HTML Template looks like:



{
  "HTML Template": {
    "prefix": "html",
    "body": [
      "<!DOCTYPE html>",
      "<html",
      "\tlang='en'",
      "\tdir='ltr'",
      "\titemscope",
      "\titemtype='https://schema.org/WebPage'",
      "\tprefix='og: http://ogp.me/ns#'",
      ">",
      "\t<head>",
      "\t\t<meta charset='UTF-8' />",
      "\t\t<meta name='viewport' content='width=device-width, initial-scale=1' />",
      "",
      "\t\t<title>$1</title>",
      "",
      "\t\t<meta name='referrer' content='origin' />",
      "\t\t<link rel='canonical' href='$0' />",
      "\t\t<link rel='icon' type='image/png' href='./icons/64x64.png' />",
      "\t\t<link rel='manifest' href='./manifest.json' />",
      "",
      "\t\t<!-- Security -->",
      "\t\t<meta http-equiv='X-Content-Type-Options' content='nosniff' />",
      "\t\t<meta http-equiv='X-XSS-Protection' content='1; mode=block' />",
      "",
      "\t\t<meta name='author' content='$3' />",
      "\t\t<meta name='description' content='$2' />",
      "\t\t<meta name='keywords' content='$4' />",
      "",
      "\t\t<meta itemprop='name' content='$1' />",
      "\t\t<meta itemprop='description' content='$2' />",
      "\t\t<meta itemprop='image' content='./icons/128x128.png' />",
      "",
      "\t\t<!-- Microsoft -->",
      "\t\t<meta http-equiv='x-ua-compatible' content='ie=edge' />",
      "\t\t<meta name='application-name' content='$1' />",
      "\t\t<meta name='msapplication-tooltip' content='$2' />",
      "\t\t<meta name='msapplication-starturl' content='/' />",
      "\t\t<meta name='msapplication-config' content='browserconfig.xml' />",
      "",
      "\t\t<!-- Facebook -->",
      "\t\t<meta property='og:type' content='website' />",
      "\t\t<meta property='og:url' content='$0' />",
      "\t\t<meta property='og:title' content='$1' />",
      "\t\t<meta property='og:image' content='./icons/256x256.png' />",
      "\t\t<meta property='og:site_name' content='$1' />",
      "\t\t<meta property='og:description' content='$2' />",
      "\t\t<meta property='og:locale' content='en_US' />",
      "",
      "\t\t<!-- Twitter -->",
      "\t\t<meta name='twitter:title' content='$1' />",
      "\t\t<meta name='twitter:description' content='$2' />",
      "\t\t<meta name='twitter:url' content='$0' />",
      "\t\t<meta name='twitter:image' content='./icons/128x128.png' />",
      "",
      "\t\t<!-- IOS -->",
      "\t\t<meta name='apple-mobile-web-app-title' content='$1' />",
      "\t\t<meta name='apple-mobile-web-app-capable' content='yes' />",
      "\t\t<meta name='apple-mobile-web-app-status-bar-style' content='#222' />",
      "\t\t<link rel='apple-touch-icon' href='./icons/256x256.png' />",
      "",
      "\t\t<!-- Android -->",
      "\t\t<meta name='theme-color' content='#eee' />",
      "\t\t<meta name='mobile-web-app-capable' content='yes' />",
      "",
      "\t\t<!-- Google Verification Tag -->",
      "",
      "\t\t<!-- Global site tag (gtag.js) - Google Analytics -->",
      "",
      "\t\t<!-- Global site tag (gtag.js) - Google Analytics -->",
      "",
      "\t\t<!-- Yandex Verification Tag -->",
      "",
      "\t\t<!-- Yandex.Metrika counter -->",
      "",
      "\t\t<!-- Mail Verification Tag -->",
      "",
      "\t\t<!-- JSON-LD -->",
      "\t\t<script type='application/ld+json'>",
      "\t\t\t{",
      "\t\t\t\t'@context': 'http://schema.org/',",
      "\t\t\t\t'@type': 'WebPage',",
      "\t\t\t\t'name': '$1',",
      "\t\t\t\t'image': [",
      "\t\t\t\t\t'$0icons/512x512.png'",
      "\t\t\t\t],",
      "\t\t\t\t'author': {",
      "\t\t\t\t\t'@type': 'Person',",
      "\t\t\t\t\t'name': '$3'",
      "\t\t\t\t},",
      "\t\t\t\t'datePublished': '2020-11-20',",
      "\t\t\t\t'description': '$2',",
      "\t\t\t\t'keywords': '$4'",
      "\t\t\t}",
      "\t\t</script>",
      "",
      "\t\t<!-- Google Fonts -->",
      "",
      "\t\t<style>",
      "\t\t\t/* Critical CSS */",
      "\t\t</style>",
      "",
      "\t\t<link rel='preload' href='./css/style.css' as='style'>",
      "\t\t<link rel='stylesheet' href='./css/style.css' />",
      "",
      "<link rel='preload' href='./script.js' as='script'>",
      "\t</head>",
      "\t<body>",
      "\t\t<!-- HTML5 -->",
      "\t\t<header>",
      "\t\t\t<h1>$1</h1>",
      "\t\t\t<nav>",
      "\t\t\t\t<a href='#' target='_blank' rel='noopener'>Link 1</a>",
      "\t\t\t\t<a href='#' target='_blank' rel='noopener'>Link 2</a>",
      "\t\t\t</nav>",
      "\t\t</header>",
      "",
      "\t\t<main></main>",
      "",
      "\t\t<footer>",
      "\t\t\t<p>ยฉ 2020. All rights reserved</p>",
      "\t\t</footer>",
      "",
      "\t\t<script src='./script.js' type='module'></script>",
      "\t</body>",
      "</html>"
    ],
    "description": "Create Modern HTML Template"
  }
}

      
      





We type html, press Tab or Enter, we get the markup. The cursor positions are defined in the following order: application name (title), description (description), author (author), keywords (keywords), address (url).



Extension



The VSCode site has excellent documentation on building extensions .



We will create two options for the extension: snippet form and CLI form. We will publish the second option in the Visual Studio Marketplace .



Examples of extensions in the form of snippets:





CLI-form extensions are less popular, probably because there are "real" CLIs.



Extension in the form of snippets


To develop extensions for VSCode, in addition to Node.js and Git , we need a couple more libraries, more precisely, one library and a plugin, namely: yeoman and generator-code . Install them globally:



npm i -g yo generator-code
// 
yarn global add yo generator-code

      
      





We execute the yo code command, select New Code Snippets, answer questions.







It remains to copy the HTML snippet we created earlier into the snippets / snippets.code-snippets file (snippet files can also have the json extension), edit the package.json and README.md, and you can publish the extension to the marketplace. As you can see, everything is very simple. Too simple, I thought, and decided to create an extension in the form of a CLI.



CLI Extension


Execute the yo code command again. This time we select New Extension (TypeScript) (do not be afraid, there will be almost no TypeScript in our code, and where it is, I will give the necessary clarifications), answer the questions.







To make sure that the extension is working, open the project in the editor:



cd htmltemplate
code .

      
      





Press F5 or the Run button (Ctrl / Cmd + Shift + D) on the left and the Start Debugging button on top. Sometimes you get an error on startup. In this case, we cancel the launch (Cancel) and repeat the procedure.



In the editor that opens, click View -> Command Palette (Ctrl / Cmd + Shift + P), type hello and select Hello World.







We receive an informational message from VSCode and a corresponding message (congratulations) in the console.







Of all the files in the project, we are interested in package.json and src / extension.ts. The src / test directory and the vsc-extension-quickstart.md file can be removed.



Let's take a look at extension.ts (comments removed for readability):



//   VSCode
import * as vscode from 'vscode'

// ,    
export function activate(context: vscode.ExtensionContext) {
  // ,    ,
  //     
  console.log('Congratulations, your extension "htmltemplate" is now active!')

  //  
  //  -   
  // htmltemplate -  
  // helloWorld -  
  let disposable = vscode.commands.registerCommand(
    'htmltemplate.helloWorld',
    () => {
      //  ,   
      //    
      vscode.window.showInformationMessage('Hello World from htmltemplate!')
    }
  )

  //  
  //   ,     "/",
  //     ""
  context.subscriptions.push(disposable)
}

// ,    
export function deactivate() {}

      
      





Important point: 'extension.command' in extension.ts must match the values โ€‹โ€‹of the activationEvents and command fields in package.json:



"activationEvents": [
  "onCommand:htmltemplate.helloWorld"
],
"contributes": {
  "commands": [
    {
      "command": "htmltemplate.helloWorld",
      "title": "Hello World"
    }
  ]
},

      
      





  • commands - list of commands
  • activationEvents - functions to be called during command execution


Let's start developing the extension.



We want our extension to resemble create-react-app or vue-cli in functionality , i.e. on the create command created a project containing all the necessary files in the target directory.



First, let's edit package.json:



"displayName": "HTML Template",
"activationEvents": [
  "onCommand:htmltemplate.create"
],
"contributes": {
  "commands": [
    {
      "command": "htmltemplate.create",
      "title": "Create Template"
    }
  ]
},

      
      





Create a src / components directory to store the project files that will be copied to the target directory.



We create project files in the form of ES6 modules (VSCode uses ES6 modules by default (export / import), but supports CommonJS modules (module.exports / require)): index.html.js, css / style.css.js , script.js, etc. File contents are exported by default:



// index.html.js
export default `
<!DOCTYPE html>
<html
  lang="en"
  dir="ltr"
  itemscope
  itemtype="https://schema.org/WebPage"
  prefix="og: http://ogp.me/ns#"
>
  ...
</html>
`

      
      





Note that with this approach all images (in our case, icons) must be Base64 encoded: here is one suitable online tool . The presence of the line "data: image / png; base64," at the beginning of the converted file is of no fundamental importance.



We will use fs-extra to copy (write) files . The outputFile method of this library does the same thing as the built-in Node.js writeFile method, but it also creates a directory for the file being written if it does not exist: for example, if we specified create css / style.css, and the css directory does not exist, outputFile will create it and will write style.css there (writeFile will throw an exception if there is no directory).



The extension.ts file looks like this:



import * as vscode from 'vscode'
//   fs-extra
const fs = require('fs-extra')
const path = require('path')

//   , ,   
import indexHTML from './components/index.html.js'
import styleCSS from './components/css/style.css.js'
import scriptJS from './components/script.js'
import icon64 from './components/icons/icon64.js'
// ...

export function activate(context: vscode.ExtensionContext) {
  console.log('Congratulations, your extension "htmltemplate" is now active!')

  let disposable = vscode.commands.registerCommand(
    'htmltemplate.create',
    () => {
      //  ,       html-template
      // filename: string  TypeScript-,
      //   ,  ,
      //   
      const folder = (filename: string) =>
        path.join(vscode.workspace.rootPath, `html-template/${filename}`)

      //    
      // files: string[] ,    files   
      const files: string[] = [
        indexHTML,
        styleCSS,
        scriptJS,
        icon64,
        ...
      ]

      //    
      //  ,        
      const fileNames: string[] = [
        'index.html',
        'css/style.css',
        'script.js',
        'server.js',
        'icons/64x64.png',
        ...
      ]

      ;(async () => {
        try {
          //    
          for (let i = 0; i < files.length; i++) {

            //  outputFile       :
            //    ( ),     (  UTF-8)

            //     png,
            // ,     Base64-:
            //   
            if (fileNames[i].includes('png')) {
              await fs.outputFile(folder(fileNames[i]), files[i], 'base64')
            // ,    
            } else {
              await fs.outputFile(folder(fileNames[i]), files[i])
            }
          }

          //     
          return vscode.window.showInformationMessage(
            'All files created successfully'
          )
        } catch {
          //   
          return vscode.window.showErrorMessage('Failed to create files')
        }
      })()
    }
  )

  context.subscriptions.push(disposable)
}

export function deactivate() {}

      
      





To prevent TypeScript from paying attention to the lack of types of imported module files, create src / global.d.ts with the following content:



declare module '*'

      
      





Let's test the extension. Open it in the editor:



cd htmltemplate
code .

      
      





Start debugging (F5). Go to the target directory (test-dir, for example) and execute the create command in the Command Palette.







We receive an informational message about the successful creation of files. Hooray!







Publishing an extension to the Visual Studio Marketplace


In order to be able to publish extensions for VSCode, you need to do the following:





Editing package.json:



{
  "name": "htmltemplate",
  "displayName": "HTML Template",
  "description": "Modern HTML Starter Template",
  "version": "1.0.0",
  "publisher": "puslisher-name",
  "license": "MIT",
  "keywords": [
    "html",
    "html5",
    "css",
    "css3",
    "javascript",
    "js"
  ],
  "icon": "build/128x128.png",
  "author": {
    "name": "Author Name @githubusername"
  },
  "repository": {
    "type": "git",
    "url": "https://github.com/username/dirname"
  },
  "engines": {
    "vscode": "^1.51.0"
  },
  "categories": [
    "Snippets"
  ],
  "activationEvents": [
    "onCommand:htmltemplate.create"
  ],
  "main": "./dist/extension.js",
  "contributes": {
    "commands": [
      {
        "command": "htmltemplate.create",
        "title": "Create Template"
      }
    ]
  },
  ...
}

      
      





Editing README.md.



Run the vsce package command in the extension directory to create a published package with the vsix extension. We get the htmltemplate-1.0.0.vsix file.



On the marketplace extensions management page, click the New extension button and select Visual Studio Code. Transfer or load the VSIX file into the modal window. We are waiting for the verification to complete.







After a green checkmark appears next to the version number, the extension becomes available for installation in VSCode.







To update the extension, you need to change the version number in package.json, generate a VSIX file and upload it to the marketplace by clicking on the More actions button and selecting Update.



As you can see, there is nothing supernatural about creating and publishing extensions for VSCode. On this, let me take my leave.



In the next part, we will create a full-fledged command line interface, first using the Heroku framework - oclif , then without it. Our Node.js-CLI will be very different from an extension, it will have some visualization, the ability to optionally initialize git and install dependencies.



I hope you found something interesting for yourself. Thank you for attention.



All Articles