├── .gitignore ├── LICENSE ├── README.md ├── bin └── convert-snippet-to-vscode.js ├── index.js ├── libs ├── plistParser.js └── snippetConverter.js └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | *.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | convert-snippets-to-vscode 2 | 3 | The MIT License (MIT) 4 | 5 | Copyright (c) Justin James 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Convert Textmate or Sublime Code Snippets to VSCode 2 | 3 | Based off the [VSCode Yeoman generator](https://github.com/Microsoft/vscode-generator-code), I have created a script to convert Textmate or Sublime snippets to a VSCode snippet json file. The intent is to be able to have multiple snippet language types in a single VSCode extension but the Yeoman template generator only does 1 snippet language. 4 | 5 | One other difference from the [VSCode Yeoman generator](https://github.com/Microsoft/vscode-generator-code), is that if will recursively look through the given directory for the snippets. 6 | 7 | ## Install the Converter 8 | 9 | ```bash 10 | npm install convert-snippets-to-vscode 11 | ``` 12 | 13 | ```bash 14 | npm install -g convert-snippets-to-vscode 15 | ``` 16 | 17 | ## Run Converter 18 | 19 | To launch the converter and be prompted for info simply type: 20 | 21 | ```bash 22 | $ snippetToVsCode 23 | 24 | Folder location that contains Text Mate (.tmSnippet) and Sublime snippets (.sublime-snippet) 25 | ? Folder name: c:\temp\Snippets\Css 26 | ? Output File Name: c:\temp\css.json 27 | ``` 28 | 29 | To launch to convert and pass in command line arguments: 30 | 31 | ```bash 32 | snippetToVsCode -s c:\temp\Snippets\Css -o c:\temp\css.json 33 | ``` 34 | ## Converter Output 35 | 36 | A json file with the converted templates in the vscode format. 37 | 38 | Take the generated file, add it to your VSCode Extension that you generated with [VSCode Yeoman generator](https://github.com/Microsoft/vscode-generator-code) in the snippets directory. 39 | 40 | Then update the package.json file in your VSCode Extension project with the new snippets file name and language that it supports. 41 | 42 | ## License 43 | 44 | [MIT](LICENSE) 45 | -------------------------------------------------------------------------------- /bin/convert-snippet-to-vscode.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var snippetConverter = require('../libs/snippetConverter'); 7 | var inquirer = require('inquirer'); 8 | 9 | var commandLineArgs = require('command-line-args'); 10 | 11 | function generateSnippet(snippetPath, outputFileName) { 12 | 13 | var convert = (function (snippetPath, outputFileName) { 14 | console.log("converting snippets"); 15 | var count = snippetConverter.processSnippetFolder(snippetPath, outputFileName); 16 | 17 | return count; 18 | }); 19 | 20 | if (snippetPath && outputFileName) { 21 | console.log("cmd args found. processing without prompt"); 22 | convert(snippetPath, outputFileName); 23 | return; 24 | } 25 | 26 | 27 | var snippetPrompt = (function () { 28 | inquirer.prompt([{ 29 | type: 'input', 30 | name: 'snippetPath', 31 | message: 'Folder name:' 32 | }, { 33 | type: 'input', 34 | name: 'outputFileName', 35 | message: 'Output File Name:' 36 | }], function (snippetAnswer) { 37 | var count = convert(snippetAnswer.snippetPath, snippetAnswer.outputFileName); 38 | if (count < 0) { 39 | snippetPrompt(); 40 | } 41 | }); 42 | }); 43 | 44 | console.log("Folder location that contains Text Mate (.tmSnippet) and Sublime snippets (.sublime-snippet)"); 45 | 46 | snippetPrompt(); 47 | 48 | 49 | } 50 | 51 | exports.generateSnippets = generateSnippet; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var commandLineArgs = require('command-line-args'); 4 | 5 | var generator = require("./bin/convert-snippet-to-vscode.js"); 6 | 7 | var cli = commandLineArgs([ 8 | { name: 'snippetPath', alias: 's', type: String }, 9 | { name: 'outputFile', alias: 'o', type: String } 10 | ]); 11 | 12 | var options = cli.parse(); 13 | if (options.snippetPath && options.outputFile) { 14 | console.log("SnippetPath: ", options.snippetPath); 15 | console.log("OutputFile: ", options.outputFile); 16 | } 17 | 18 | generator.generateSnippets(options.snippetPath, options.outputFile); -------------------------------------------------------------------------------- /libs/plistParser.js: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------- 2 | * Copyright (C) Microsoft Corporation. All rights reserved. 3 | *--------------------------------------------------------*/ 4 | 'use strict'; 5 | var createParser = (function () { 6 | var saxModule = null; 7 | return function parser(strict, opt) { 8 | if (!saxModule) { 9 | saxModule = require('sax'); 10 | } 11 | return saxModule.parser(strict, opt); 12 | }; 13 | })(); 14 | function parse(content) { 15 | var errors = []; 16 | var currObject = null; 17 | var result = null; 18 | var text = null; 19 | var parser = createParser(false, { lowercase: true }); 20 | parser.onerror = function (e) { 21 | errors.push(e.message); 22 | }; 23 | parser.ontext = function (s) { 24 | text += s; 25 | }; 26 | parser.oncdata = function (s) { 27 | text += s; 28 | }; 29 | parser.onopentag = function (tag) { 30 | switch (tag.name) { 31 | case 'dict': 32 | currObject = { parent: currObject, value: {} }; 33 | break; 34 | case 'array': 35 | currObject = { parent: currObject, value: [] }; 36 | break; 37 | case 'key': 38 | if (currObject) { 39 | currObject.lastKey = null; 40 | } 41 | break; 42 | } 43 | text = ''; 44 | }; 45 | parser.onclosetag = function (tagName) { 46 | var value; 47 | switch (tagName) { 48 | case 'key': 49 | if (!currObject || Array.isArray(currObject.value)) { 50 | errors.push('key can only be used inside an open dict element'); 51 | return; 52 | } 53 | currObject.lastKey = text; 54 | return; 55 | case 'dict': 56 | case 'array': 57 | if (!currObject) { 58 | errors.push(tagName + ' closing tag found, without opening tag'); 59 | return; 60 | } 61 | value = currObject.value; 62 | currObject = currObject.parent; 63 | break; 64 | case 'string': 65 | case 'data': 66 | value = text; 67 | break; 68 | case 'date': 69 | value = new Date(text); 70 | break; 71 | case 'integer': 72 | value = parseInt(text); 73 | if (isNaN(value)) { 74 | errors.push(text + ' is not a integer'); 75 | return; 76 | } 77 | break; 78 | case 'real': 79 | value = parseFloat(text); 80 | if (isNaN(value)) { 81 | errors.push(text + ' is not a float'); 82 | return; 83 | } 84 | break; 85 | case 'true': 86 | value = true; 87 | break; 88 | case 'false': 89 | value = false; 90 | break; 91 | case 'plist': 92 | return; 93 | default: 94 | errors.push('Invalid tag name: ' + tagName); 95 | return; 96 | } 97 | if (!currObject) { 98 | result = value; 99 | } 100 | else if (Array.isArray(currObject.value)) { 101 | currObject.value.push(value); 102 | } 103 | else { 104 | if (currObject.lastKey) { 105 | currObject.value[currObject.lastKey] = value; 106 | } 107 | else { 108 | errors.push('Dictionary key missing for value ' + value); 109 | } 110 | } 111 | }; 112 | parser.write(content); 113 | return { errors: errors, value: result }; 114 | } 115 | exports.parse = parse; -------------------------------------------------------------------------------- /libs/snippetConverter.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var plistParser = require('./plistParser'); 6 | var sax = require('sax'); 7 | 8 | function processSnippetFolder(folderPath, outputFileName) { 9 | var errors = [], snippets = {}; 10 | var snippetCount = 0; 11 | var languageId = null; 12 | 13 | var count = convert(folderPath, snippets, errors); 14 | if (count <= 0) { 15 | console.log("No valid snippets found in " + folderPath + (errors.length > 0 ? '.\n' + errors.join('\n'): '')); 16 | return count; 17 | } 18 | 19 | var jsonOutput = JSON.stringify(snippets, null, '\t'); 20 | 21 | // Bug Fix: change \\$ to just $. Json Stringify changes the \$ in the sublime template to \\$ so VSCode inserts \$ instead of $ when using the snippets. 22 | fs.writeFile(outputFileName, jsonOutput.replace(/\\\\\$/g, '\$'), function (err) { 23 | if (err) throw err; 24 | console.log('File wrote to ' + outputFileName); 25 | }); 26 | console.log(count + " snippet(s) found and converted." + (errors.length > 0 ? '\n\nProblems while converting: \n' + errors.join('\n'): '')); 27 | return count; 28 | 29 | function convert(folderPath) { 30 | 31 | var files = []; 32 | getFolderContent(folderPath, files,errors); 33 | if (errors.length > 0) { 34 | return -1; 35 | } 36 | 37 | files.forEach(function (fileName) { 38 | var extension = path.extname(fileName).toLowerCase(); 39 | var snippet; 40 | if (extension === '.tmsnippet') { 41 | snippet = convertTextMate(fileName); 42 | } else if (extension === '.sublime-snippet') { 43 | snippet = convertSublime(fileName); 44 | } 45 | // console.log(snippet); 46 | if (snippet) { 47 | if (snippet.prefix && snippet.body) { 48 | snippets[getId(snippet.prefix)] = snippet; 49 | snippetCount++; 50 | guessLanguage(snippet.scope); 51 | } else { 52 | var filePath = fileName; 53 | if (!snippet.prefix) { 54 | errors.push(filePath + ": Missing property 'tabTrigger'. Snippet skipped."); 55 | } else { 56 | errors.push(filePath + ": Missing property 'content'. Snippet skipped."); 57 | } 58 | } 59 | } 60 | 61 | }); 62 | return snippetCount; 63 | } 64 | 65 | 66 | function getId(prefix) { 67 | if (snippets.hasOwnProperty(prefix)) { 68 | var counter = 1; 69 | while (snippets.hasOwnProperty(prefix + counter)) { 70 | counter++; 71 | } 72 | return prefix + counter; 73 | } 74 | return prefix; 75 | } 76 | 77 | function guessLanguage(scopeName) { 78 | if (!languageId && scopeName) { 79 | var match; 80 | if (match = /(source|text)\.(\w+)/.exec(scopeName)) { 81 | languageId = match[2]; 82 | } 83 | } 84 | } 85 | 86 | function convertTextMate(filePath) { 87 | var body = getFileContent(filePath, errors); 88 | if (!body) { 89 | return; 90 | } 91 | 92 | var result = plistParser.parse(body); 93 | if (result.errors) { 94 | Array.prototype.push.apply(errors, result.errors) 95 | } 96 | var value = result.value; 97 | 98 | return { 99 | prefix: value.tabTrigger, 100 | body: value.content, 101 | description: value.name, 102 | scope: value.scope 103 | } 104 | } 105 | 106 | function convertSublime(filePath) { 107 | var body = getFileContent(filePath, errors); 108 | if (!body) { 109 | return; 110 | } 111 | 112 | var parser = sax.parser(false, { lowercase: true }); 113 | var text = null; 114 | var snippet = { 115 | prefix: '', 116 | body: '', 117 | description: '', 118 | scope: '' 119 | }; 120 | 121 | parser.onerror = function (e) { 122 | errors.push(filePath + ": Problems parsing content content: " + e.message); 123 | }; 124 | parser.ontext = function (s) { 125 | text += s; 126 | }; 127 | parser.oncdata = function (s) { 128 | text += s; 129 | }; 130 | parser.onopentag = function (tag) { 131 | text = ''; 132 | }; 133 | parser.onclosetag = function (tagName) { 134 | switch (tagName) { 135 | case 'tabtrigger': 136 | snippet.prefix = text; 137 | break; 138 | case 'content': 139 | // console.log('text', text); 140 | snippet.body = text; 141 | break; 142 | case 'description': 143 | snippet.description = text; 144 | break; 145 | case 'scope': 146 | snippet.scope = text; 147 | break; 148 | } 149 | 150 | } 151 | 152 | parser.write(body); 153 | // console.log(snippet); 154 | return snippet; 155 | } 156 | 157 | 158 | } 159 | 160 | function getFolderContent(folderPath, filelist, errors) { 161 | try { 162 | if( folderPath[folderPath.length-1] != '/') folderPath=folderPath.concat('/'); 163 | var files = fs.readdirSync(folderPath); 164 | filelist = filelist || []; 165 | files.forEach(function(file){ 166 | if (fs.statSync(path.join(folderPath,file)).isDirectory()){ 167 | filelist = getFolderContent(path.join(folderPath,file), filelist, errors); 168 | } else { 169 | filelist.push (path.join(folderPath, file)); 170 | } 171 | }); 172 | 173 | return filelist; 174 | } catch (e) { 175 | errors.push("Unable to access " + folderPath + ": " + e.message); 176 | return []; 177 | } 178 | } 179 | 180 | function getFileContent(filePath, errors) { 181 | try { 182 | var content = fs.readFileSync(filePath).toString(); 183 | if (content === '') { 184 | errors.push(filePath + ": Empty file content"); 185 | } 186 | // console.log(content); 187 | return content; 188 | } catch (e) { 189 | errors.push(filePath + ": Problems loading file content: " + e.message); 190 | return null; 191 | } 192 | } 193 | 194 | function isFile(filePath) { 195 | try { 196 | return fs.statSync(filePath).isFile() 197 | } catch (e) { 198 | return false; 199 | } 200 | } 201 | 202 | exports.processSnippetFolder = processSnippetFolder; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "convert-snippets-to-vscode", 3 | "version": "1.0.2", 4 | "description": "Convert Sublime and Textmate Snippets to Visual Studio Code Format", 5 | "keywords": [ 6 | "sublime", 7 | "textmate", 8 | "vscode", 9 | "visual studio", 10 | "visual studio code", 11 | "vs code", 12 | "extensions", 13 | "snippets" 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/digitaldrummerj/convert-snippets-to-vscode.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/digitaldrummerj/convert-snippets-to-vscode/issues" 21 | }, 22 | "homepage": "http://digitaldrummerj.me/convert-snippets-to-vscode/", 23 | "license": "MIT", 24 | "author": { 25 | "name": "Justin James", 26 | "url": "https://digitaldrummerj.me" 27 | }, 28 | "engines": { 29 | "node": ">=0.10.0" 30 | }, 31 | "dependencies": { 32 | "command-line-args": "^2.1.6", 33 | "inquirer": "^0.12.0", 34 | "sax": "^1.1.3" 35 | }, 36 | "main": "./index.js", 37 | "bin": { 38 | "snippetToVsCode": "./index.js" 39 | } 40 | } 41 | --------------------------------------------------------------------------------