├── .eslintrc.json ├── .gitignore ├── .importjs.js ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── extension.js ├── jsconfig.json ├── lib ├── daemon.js ├── project.js └── textEditsForDiff.js ├── package.json ├── test ├── extension.test.js └── index.js └── vsc-extension-quickstart.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "warn", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "warn", 20 | "constructor-super": "warn", 21 | "valid-typeof": "warn" 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.importjs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | environments: ['node'], 3 | declarationKeyword: 'const', 4 | logLevel: 'debug', 5 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}" ], 11 | "stopOnEntry": false 12 | }, 13 | { 14 | "name": "Launch Tests", 15 | "type": "extensionHost", 16 | "request": "launch", 17 | "runtimeExecutable": "${execPath}", 18 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/test" ], 19 | "stopOnEntry": false 20 | } 21 | ] 22 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | jsconfig.json 6 | vsc-extension-quickstart.md 7 | .eslintrc.json 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to the "vscode-import-js" extension will be documented in this file. 3 | 4 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 5 | 6 | ## [Unreleased] 7 | - Initial release -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # VS Code ImportJS extension 2 | 3 | This is the VS Code extension for ImportJS. 4 | 5 | ## Installing 6 | 7 | 1. Install `import-js` globally: 8 | 9 | ```bash 10 | npm install --global import-js 11 | ``` 12 | 13 | 2. [Configure ImportJS](https://github.com/galooshi/import-js#configuration) for your project 14 | 15 | 3. Open the root directory of your project (File -> Open…) 16 | 17 | 4. Import a file! 18 | 19 | ## Default keybindings 20 | 21 | By default, `vscode-import-js` attempts to set up the following keybindings: 22 | 23 | Mapping | Action | Description 24 | --------------|-------------|--------------------------------------------------------------------- 25 | `Cmd+Shift+j` | Import word | Import the module for the variable under the cursor. 26 | `Cmd+Shift+i` | Fix imports | Import any missing modules and remove any modules that are not used. 27 | `Cmd+Shift+k` | Go to word | Go to the module of the variable under the cursor. -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | 3 | const { 4 | currentFilePath, 5 | currentWord, 6 | syncFileContents 7 | } = require("./lib/project"); 8 | const { start, kill, run, on } = require("./lib/daemon"); 9 | 10 | const STATUS_BAR_DELAY = 3000; 11 | 12 | function handleMessage(json) { 13 | const { messages, fileContent, unresolvedImports, error, goto } = json; 14 | 15 | if (error) { 16 | vscode.window.showWarningMessage( 17 | `ImportJS encountered an error: ${error.message}` 18 | ); 19 | return; 20 | } 21 | 22 | if (messages && messages.length > 0) { 23 | vscode.window.setStatusBarMessage(messages.join("\n"), STATUS_BAR_DELAY); 24 | } 25 | 26 | if (goto) { 27 | vscode.workspace.openTextDocument(goto).then(document => { 28 | vscode.window.showTextDocument(document); 29 | }); 30 | return; 31 | } 32 | 33 | // Always sync resolved imports, even if there are some remaining to resolve 34 | if ("fileContent" in json) { 35 | syncFileContents(fileContent); 36 | } 37 | 38 | if (unresolvedImports && Object.keys(unresolvedImports).length > 0) { 39 | const imports = Object.keys(unresolvedImports).map(name => { 40 | return { name, options: unresolvedImports[name] }; 41 | }); 42 | 43 | // Ask the user to resolve each import, one at a time. When all imports are 44 | // resolved, or when the user cancels, the chain of promises will resolve. 45 | const requestResolutions = (remaining, resolutions = []) => { 46 | if (remaining.length === 0) { 47 | return Promise.resolve(resolutions); 48 | } 49 | 50 | const { name, options } = remaining[0]; 51 | 52 | let pickerItems = options 53 | // Filter out duplicates 54 | .reduce((uniqueImports, option) => { 55 | const duplicate = uniqueImports.find( 56 | ({ displayName }) => option.displayName === displayName 57 | ); 58 | 59 | if (duplicate) { 60 | return uniqueImports; 61 | } 62 | 63 | return uniqueImports.concat(option); 64 | }, []) 65 | .map(({ displayName, data }) => { 66 | return { 67 | label: displayName, 68 | data 69 | }; 70 | }); 71 | 72 | return vscode.window.showQuickPick(pickerItems).then(selected => { 73 | // If user cancels, still import the modules they've resolved so far 74 | if (!selected) { 75 | return Promise.resolve(resolutions); 76 | } 77 | 78 | const { data } = selected; 79 | const resolution = { 80 | name, 81 | data 82 | }; 83 | 84 | return requestResolutions( 85 | remaining.slice(1), 86 | resolutions.concat(resolution) 87 | ); 88 | }); 89 | }; 90 | 91 | requestResolutions(imports).then(resolutions => { 92 | const imports = resolutions.reduce((imports, resolution) => { 93 | const { name, data } = resolution; 94 | imports[name] = data; 95 | return imports; 96 | }, {}); 97 | 98 | run("add", imports); 99 | }); 100 | } 101 | } 102 | 103 | function activate(context) { 104 | const subscriptions = [ 105 | vscode.commands.registerCommand("importjs.word", () => 106 | run("word", currentWord()) 107 | ), 108 | vscode.commands.registerCommand("importjs.goto", () => 109 | run("goto", currentWord()) 110 | ), 111 | vscode.commands.registerCommand("importjs.fix", () => run("fix")) 112 | ]; 113 | 114 | subscriptions.forEach(sub => context.subscriptions.push(sub)); 115 | 116 | on("message", handleMessage); 117 | 118 | start(); 119 | } 120 | 121 | function deactivate() { 122 | kill(); 123 | } 124 | 125 | exports.activate = activate; 126 | exports.deactivate = deactivate; 127 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": [ 6 | "es6" 7 | ], 8 | "experimentalDecorators": true 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /lib/daemon.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const oboe = require("oboe"); 3 | const { spawn, exec } = require("child_process"); 4 | const { EventEmitter } = require("events"); 5 | 6 | const { 7 | projectDirectoryPath, 8 | currentFilePath, 9 | currentFileContents 10 | } = require("./project"); 11 | 12 | let daemon = null; 13 | 14 | const emitter = new EventEmitter(); 15 | const on = emitter.on.bind(emitter); 16 | 17 | function getPath() { 18 | return new Promise((resolve, reject) => { 19 | try { 20 | exec("which importjs", (error, stdout) => { 21 | if (error) { 22 | reject(error); 23 | } else { 24 | resolve(stdout); 25 | } 26 | }); 27 | } catch (error) { 28 | reject(error); 29 | } 30 | }); 31 | } 32 | 33 | function install() { 34 | return new Promise((resolve, reject) => { 35 | try { 36 | const npmExecutable = /^win/.test(process.platform) ? "npm.cmd" : "npm"; 37 | const npm = spawn(npmExecutable, ["install", "-g", "import-js"], { 38 | cwd: projectDirectoryPath() 39 | }); 40 | npm.on("error", error => reject(error)); 41 | npm.on("close", () => resolve()); 42 | } catch (error) { 43 | reject(error); 44 | } 45 | }); 46 | } 47 | 48 | function tryInstall() { 49 | vscode.window.showInformationMessage("Installing ImportJS"); 50 | 51 | return install() 52 | .then(() => { 53 | vscode.window.showInformationMessage("ImportJS installed!"); 54 | }) 55 | .catch(error => { 56 | vscode.window.showInformationMessage( 57 | `Failed to install ImportJS: ${error.message}` 58 | ); 59 | }); 60 | } 61 | 62 | function handleError(error) { 63 | daemon = null; 64 | console.warn("Failed to start ImportJS server", error); 65 | 66 | getPath() 67 | .then(() => { 68 | vscode.window.showWarningMessage( 69 | `Failed to start ImportJS server: ${error.message}` 70 | ); 71 | }) 72 | .catch(() => { 73 | const installMessage = "Install globally via npm"; 74 | 75 | vscode.window 76 | .showWarningMessage( 77 | `Failed to start ImportJS server: importjs not found in PATH`, 78 | installMessage 79 | ) 80 | .then(selected => { 81 | if (selected === installMessage) { 82 | tryInstall(); 83 | } 84 | }); 85 | }); 86 | } 87 | 88 | function start() { 89 | try { 90 | const daemonExecutable = /^win/.test(process.platform) ? "importjs.cmd" : "importjs"; 91 | daemon = spawn(daemonExecutable, ["start", `--parent-pid=${process.pid}`], { 92 | cwd: projectDirectoryPath() 93 | }); 94 | daemon.on("error", handleError); 95 | daemon.on("close", () => daemon = null); 96 | } catch (error) { 97 | handleError(error); 98 | return; 99 | } 100 | 101 | // Ignore the first message passed. This just gives the log path, and isn't json. 102 | daemon.stdout.once("data", () => { 103 | // After the first message, handle top-level JSON objects 104 | oboe(daemon.stdout).node("!", obj => { 105 | emitter.emit("message", obj); 106 | }); 107 | }); 108 | } 109 | 110 | function run(command, commandArg) { 111 | if (!daemon) { 112 | start(); 113 | } 114 | 115 | const payload = { 116 | command: command, 117 | commandArg: commandArg, 118 | pathToFile: currentFilePath(), 119 | fileContent: currentFileContents() 120 | }; 121 | 122 | daemon.stdin.write(JSON.stringify(payload) + "\n"); 123 | } 124 | 125 | function isRunning() { 126 | return !!daemon; 127 | } 128 | 129 | function kill() { 130 | if (daemon) { 131 | daemon.kill(); 132 | daemon = null; 133 | } 134 | } 135 | 136 | exports.isRunning = isRunning; 137 | exports.start = start; 138 | exports.kill = kill; 139 | exports.run = run; 140 | exports.on = on; 141 | -------------------------------------------------------------------------------- /lib/project.js: -------------------------------------------------------------------------------- 1 | const vscode = require("vscode"); 2 | const { WorkspaceEdit } = vscode; 3 | 4 | const textEditsForDiff = require("./textEditsForDiff"); 5 | 6 | function activeEditor() { 7 | return vscode.window.activeTextEditor; 8 | } 9 | 10 | function currentDocument() { 11 | return activeEditor().document; 12 | } 13 | 14 | function currentFilePath() { 15 | return currentDocument().fileName; 16 | } 17 | 18 | function currentFileContents() { 19 | return currentDocument().getText(); 20 | } 21 | 22 | function projectDirectoryPath() { 23 | return vscode.workspace.rootPath; 24 | } 25 | 26 | function applyEdits(edits) { 27 | edits.forEach(edit => { 28 | const workspaceEdit = new WorkspaceEdit(); 29 | workspaceEdit.set(currentDocument().uri, [edit]); 30 | vscode.workspace.applyEdit(workspaceEdit); 31 | }) 32 | } 33 | 34 | function syncFileContents(newText) { 35 | const currentText = currentFileContents(); 36 | 37 | if (currentText === newText) return; 38 | 39 | const textEdits = textEditsForDiff(currentText, newText); 40 | applyEdits(textEdits); 41 | } 42 | 43 | // Detect word boundaries in JavaScript 44 | // https://github.com/codemirror/CodeMirror/blob/master/mode/javascript/javascript.js#L25 45 | const wordRE = /[\w$\xa1-\uffff]+/; 46 | 47 | function currentWord() { 48 | const position = activeEditor().selection.active; 49 | const range = currentDocument().getWordRangeAtPosition(position, wordRE); 50 | 51 | return range ? currentDocument().getText(range) : null; 52 | } 53 | 54 | exports.activeEditor = activeEditor; 55 | exports.currentDocument = currentDocument; 56 | exports.currentFilePath = currentFilePath; 57 | exports.currentFileContents = currentFileContents; 58 | exports.applyEdits = applyEdits; 59 | exports.syncFileContents = syncFileContents; 60 | exports.currentWord = currentWord; 61 | exports.projectDirectoryPath = projectDirectoryPath; 62 | -------------------------------------------------------------------------------- /lib/textEditsForDiff.js: -------------------------------------------------------------------------------- 1 | // Adapted from Atom TextBuffer setTextViaDiff implementation 2 | // https://github.com/atom/text-buffer/blob/141e35614ff4a0bed3c113782ee58029a53de775/src/text-buffer.coffee#L640 3 | 4 | // Copyright (c) 2013 GitHub Inc. 5 | 6 | // Permission is hereby granted, free of charge, to any person obtaining a copy of 7 | // this software and associated documentation files (the "Software"), to deal in 8 | // the Software without restriction, including without limitation the rights to 9 | // use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 10 | // the Software, and to permit persons to whom the Software is furnished to do so, 11 | // subject to the following conditions: 12 | 13 | // The above copyright notice and this permission notice shall be included in all 14 | // copies or substantial portions of the Software. 15 | 16 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 18 | // FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 19 | // COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 20 | // IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 21 | // CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 22 | 23 | const { Position, Range, TextEdit } = require("vscode"); 24 | const diff = require("diff"); 25 | 26 | const newlineRegex = /\r\n|\n|\r/g; 27 | 28 | const endsWithNewline = str => /[\r\n]+$/g.test(str); 29 | 30 | const computeBufferColumn = str => { 31 | const newlineIndex = Math.max(str.lastIndexOf("\n"), str.lastIndexOf("\r")); 32 | 33 | if (endsWithNewline(str)) { 34 | return 0; 35 | } else if (newlineIndex === -1) { 36 | return str.length; 37 | } else { 38 | return str.length - newlineIndex - 1; 39 | } 40 | }; 41 | 42 | function textEditsForDiff(originalText, newText) { 43 | let row = 0; 44 | let column = 0; 45 | 46 | const lineDiff = diff.diffLines(originalText, newText); 47 | const edits = []; 48 | 49 | lineDiff.forEach(change => { 50 | // Using change.count does not account for lone carriage-returns 51 | const match = change.value.match(newlineRegex); 52 | const lineCount = match ? match.length : 0; 53 | 54 | if (change.added) { 55 | const range = new Range( 56 | new Position(row, column), 57 | new Position(row, column) 58 | ); 59 | 60 | edits.push(new TextEdit(range, change.value)); 61 | 62 | row += lineCount; 63 | column = computeBufferColumn(change.value); 64 | } else if (change.removed) { 65 | const endRow = row + lineCount; 66 | const endColumn = column + computeBufferColumn(change.value); 67 | 68 | const range = new Range( 69 | new Position(row, column), 70 | new Position(endRow, endColumn) 71 | ); 72 | 73 | edits.push(new TextEdit(range, "")); 74 | } else { 75 | row += lineCount; 76 | column = computeBufferColumn(change.value); 77 | } 78 | }); 79 | 80 | return edits; 81 | } 82 | 83 | module.exports = textEditsForDiff; 84 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-import-js", 3 | "displayName": "ImportJS", 4 | "description": "VSCode extension for ImportJS, a tool that helps you import javascript dependencies by automatically injecting import statements at the top of the file.", 5 | "version": "0.0.9", 6 | "publisher": "dabbott", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/dabbott/vscode-import-js" 10 | }, 11 | "galleryBanner": { 12 | "color": "#00A699", 13 | "theme": "dark" 14 | }, 15 | "engines": { 16 | "vscode": "^1.12.0" 17 | }, 18 | "categories": [ 19 | "Other" 20 | ], 21 | "keywords": [ 22 | "import-js", 23 | "import", 24 | "vscode-import-js" 25 | ], 26 | "activationEvents": [ 27 | "onLanguage:javascript", 28 | "onLanguage:javascriptreact" 29 | ], 30 | "main": "./extension", 31 | "contributes": { 32 | "commands": [ 33 | { 34 | "command": "importjs.word", 35 | "title": "ImportJS: Import word under cursor" 36 | }, 37 | { 38 | "command": "importjs.goto", 39 | "title": "ImportJS: Go to module" 40 | }, 41 | { 42 | "command": "importjs.fix", 43 | "title": "ImportJS: Fix imports" 44 | } 45 | ], 46 | "keybindings": [ 47 | { 48 | "command": "importjs.word", 49 | "key": "ctrl+shift+j", 50 | "mac": "cmd+shift+j", 51 | "when": "editorTextFocus" 52 | }, 53 | { 54 | "command": "importjs.goto", 55 | "key": "ctrl+shift+k", 56 | "mac": "cmd+shift+k", 57 | "when": "editorTextFocus" 58 | }, 59 | { 60 | "command": "importjs.fix", 61 | "key": "ctrl+shift+i", 62 | "mac": "cmd+shift+i", 63 | "when": "editorTextFocus" 64 | } 65 | ] 66 | }, 67 | "scripts": { 68 | "postinstall": "node ./node_modules/vscode/bin/install", 69 | "test": "node ./node_modules/vscode/bin/test" 70 | }, 71 | "dependencies": { 72 | "diff": "^3.2.0", 73 | "import-js": "^2.3.0", 74 | "oboe": "^2.1.3" 75 | }, 76 | "devDependencies": { 77 | "typescript": "^2.0.3", 78 | "vscode": "^1.0.0", 79 | "mocha": "^2.3.3", 80 | "eslint": "^3.6.0", 81 | "@types/node": "^6.0.40", 82 | "@types/mocha": "^2.2.32" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/extension.test.js: -------------------------------------------------------------------------------- 1 | /* global suite, test */ 2 | 3 | // 4 | // Note: This example test is leveraging the Mocha test framework. 5 | // Please refer to their documentation on https://mochajs.org/ for help. 6 | // 7 | 8 | // The module 'assert' provides assertion methods from node 9 | var assert = require('assert'); 10 | 11 | // You can import and use all API from the 'vscode' module 12 | // as well as import your extension to test it 13 | var vscode = require('vscode'); 14 | var myExtension = require('../extension'); 15 | 16 | // Defines a Mocha test suite to group tests of similar kind together 17 | suite("Extension Tests", function() { 18 | 19 | // Defines a Mocha unit test 20 | test("Something 1", function() { 21 | assert.equal(-1, [1, 2, 3].indexOf(5)); 22 | assert.equal(-1, [1, 2, 3].indexOf(0)); 23 | }); 24 | }); -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | var testRunner = require('vscode/lib/testrunner'); 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.js (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your first VS Code Extension 2 | 3 | ## What's in the folder 4 | * This folder contains all of the files necessary for your extension 5 | * `package.json` - this is the manifest file in which you declare your extension and command. 6 | The sample plugin registers a command and defines its title and command name. With this information 7 | VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `extension.js` - this is the main file where you will provide the implementation of your command. 9 | The file exports one function, `activate`, which is called the very first time your extension is 10 | activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 11 | We pass the function containing the implementation of the command as the second parameter to 12 | `registerCommand`. 13 | 14 | ## Get up and running straight away 15 | * press `F5` to open a new window with your extension loaded 16 | * run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World` 17 | * set breakpoints in your code inside extension.ts to debug your extension 18 | * find output from your extension in the debug console 19 | 20 | ## Make changes 21 | * you can relaunch the extension from the debug toolbar after changing code in `extension.js` 22 | * you can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes 23 | 24 | ## Explore the API 25 | * you can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts` 26 | 27 | ## Run tests 28 | * open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests` 29 | * press `F5` to run the tests in a new window with your extension loaded 30 | * see the output of the test result in the debug console 31 | * make changes to `test/extension.test.js` or create new test files inside the `test` folder 32 | * by convention, the test runner will only consider files matching the name pattern `**.test.js` 33 | * you can create folders inside the `test` folder to structure your tests any way you want --------------------------------------------------------------------------------