├── .eslintrc ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── package.json ├── src └── main.ts └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "ecmaVersion": 2018, 6 | "sourceType": "module" 7 | }, 8 | "plugins": ["@typescript-eslint"], 9 | "env": { 10 | "browser": true, 11 | "node": true, 12 | "es6": true 13 | }, 14 | "extends": [ 15 | "eslint:recommended", 16 | "plugin:@typescript-eslint/recommended", 17 | "prettier", 18 | "prettier/@typescript-eslint" 19 | ], 20 | "rules": { 21 | "@typescript-eslint/no-explicit-any": 0, 22 | "@typescript-eslint/no-unused-vars": [ 23 | "warn", 24 | { 25 | "argsIgnorePattern": "^_" 26 | } 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /out 2 | /node_modules 3 | /.vscode-test/ 4 | *.vsix 5 | /package-lock.json 6 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "ms-vscode.vscode-typescript-tslint-plugin" 6 | ] 7 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ], 19 | "preLaunchTask": "npm: watch" 20 | }, 21 | { 22 | "name": "Extension Tests", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}", 28 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 29 | ], 30 | "outFiles": [ 31 | "${workspaceFolder}/out/test/**/*.js" 32 | ], 33 | "preLaunchTask": "npm: watch" 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project 6 | adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.1.0] - 2019-08-12 9 | 10 | - Initial release. 11 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, gender identity and expression, level of experience, 9 | education, socio-economic status, nationality, personal appearance, race, 10 | religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at admin@immutable.rs. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-use-package 2 | 3 | Programmatic configuration, extension management and keybinding for VS Code, to go with the 4 | [init-script](https://github.com/bodil/vscode-init-script) extension. 5 | 6 | This is heavily inspired by John Wiegley's [use-package](https://github.com/jwiegley/use-package) 7 | system for Emacs. 8 | 9 | ## Usage 10 | 11 | `vscode-use-package` provides the `usePackage` function, which takes care of installing and 12 | configuring extensions from your `init.ts` file. The advantage of this is that you can easily 13 | maintain (and, perhaps more importantly, keep in version control) a consistent VS Code configuration 14 | across multiple computers and OS installations. 15 | 16 | The recommended way to install this is using `npm` in the folder where you keep your `init.ts` 17 | script: 18 | 19 | ```sh 20 | $ cd ~/.config/Code/User # or equivalent 21 | $ npm add vscode-use-package 22 | ``` 23 | 24 | In this way, you can simply `import` it in your `init.ts` file (or `require` it in your `init.js` 25 | file, if you prefer): 26 | 27 | ```js 28 | import { initUsePackage, usePackage } from "vscode-use-package"; 29 | ``` 30 | 31 | ### `usePackage` 32 | 33 | `usePackage` takes a package name (`.` as found in the "Installation" 34 | header on the Marketplace page) and an optional configuration object. It will check if the extension 35 | is already installed, install it for you if it's not, then go ahead and configure it according to 36 | your specifications. 37 | 38 | The configuration object looks like this (and all the keys are optional): 39 | 40 | ```typescript 41 | export type UsePackageOptions = { 42 | local?: boolean; 43 | remote?: string | boolean; 44 | scope?: string; 45 | config?: Record; 46 | globalConfig?: Record; 47 | keymap?: Array; 48 | init?: () => Thenable; 49 | }; 50 | ``` 51 | 52 | #### `local` and `remote` 53 | 54 | If you pass the option `local: true`, this extension will not install on 55 | [remotes](https://code.visualstudio.com/docs/remote/remote-overview) such as WSL or an SSH host. If 56 | you pass `remote: true`, it will only install if you're running on a remote. This is useful if you 57 | have a single init script for all your development environments, but you use extensions which aren't 58 | designed to run either locally or remotely. 59 | 60 | Specifically, UI extensions have to be installed locally and cannot be installed at all by an init 61 | script running on a remote instance. You should therefore flag UI extensions `local: true` and make 62 | sure you run your init script locally before connecting to a remote host. 63 | 64 | With the `remote` property, you can also specify a string which will be matched against 65 | `vscode.env.remoteName`, so that you can eg. use `remote: "wsl"` to restrict an extension to only 66 | install on WSL remotes. Check the 67 | [VS Code API documentation](https://code.visualstudio.com/api/references/vscode-api#env) for more 68 | info on `vscode.env.remoteName`. 69 | 70 | #### `config` 71 | 72 | The `config` property takes an object of configuration keys and values, and updates the VS Code 73 | configuration accordingly. The keys will be automatically namespaced to the package you're 74 | configuring: `usePackage("my-package", {config: {enableFeature: true}})` will result in the 75 | configuration key `my-package.enableFeature` being set to `true`. 76 | 77 | If the name of the configuration scope differs from the name of the package, as, unfortunately, 78 | often happens, you can use the `scope` property to override it. 79 | 80 | As an example, here is how you'd install the 81 | [GitLens](https://marketplace.visualstudio.com/items?itemName=eamodio.gitlens) package and configure 82 | it to stop showing you annotations for the current line: 83 | 84 | ```typescript 85 | usePackage("eamodio.gitlens", { 86 | config: { 87 | "currentLine.enabled": false, 88 | }, 89 | }); 90 | ``` 91 | 92 | #### `globalConfig` 93 | 94 | If you need to set options outside the package scope, use the `globalConfig` property, and feed it 95 | fully namespaced keys, as you would in `settings.json`. 96 | 97 | For instance, this is how you could install a new syntax theme and enable it automatically: 98 | 99 | ```typescript 100 | usePackage("jack-pallot.atom-dark-syntax", { 101 | globalConfig: { "workbench.colorTheme": "Atom Dark Syntax" }, 102 | }); 103 | ``` 104 | 105 | #### `keymap` 106 | 107 | The `keymap` property is used to define keybindings for the package. The commands, unlike the 108 | settings keys, are not automatically prefixed with the package scope. The format is (or is supposed 109 | to be) equivalent to `keybindings.json`. 110 | 111 | The `Keybinding` type looks like this: 112 | 113 | ```typescript 114 | export type Keybinding = { 115 | key: string; 116 | command: string; 117 | when?: string; 118 | args?: Record; 119 | }; 120 | ``` 121 | 122 | Here is how you use it: 123 | 124 | ```typescript 125 | usePackage("garaemon.vscode-emacs-tab", { 126 | scope: "emacs-tab", 127 | keymap: [ 128 | { 129 | key: "tab", 130 | command: "emacs-tab.reindentCurrentLine", 131 | when: "editorTextFocus", 132 | }, 133 | ], 134 | }); 135 | ``` 136 | 137 | #### `init` 138 | 139 | The `init` property takes a function which will be called once the package is installed and 140 | everything else is configured, in case you need to do any configuration that isn't covered by the 141 | other properties. 142 | 143 | ```typescript 144 | usePackage("jack-pallot.atom-dark-syntax", { 145 | globalConfig: { "workbench.colorTheme": "Atom Dark Syntax" }, 146 | init: () => alert("syntax theme installed!"), // please don't do this, though 147 | }); 148 | ``` 149 | 150 | ### `configSet` and `keymapSet` 151 | 152 | In addition to `usePackage`, the `vscode-use-package` module exports the 153 | `configSet(scope: string, options: Record)` function and the 154 | `keymapSet(keymap: Array)` function. These are the function `usePackage` calls to set 155 | config options and keybindings, but you might want to use these to configure settings unrelated to 156 | extensions: 157 | 158 | ```typescript 159 | configSet("workbench", { 160 | "editor.showTabs": false, 161 | "editor.enablePreview": false, 162 | "activityBar.visible": true, 163 | }); 164 | 165 | keymapSet([ 166 | { 167 | key: "ctrl+x ctrl+c", 168 | command: "workbench.action.quit", 169 | }, 170 | ]); 171 | ``` 172 | 173 | Note that you can also call `configSet(options: Record)` without the scope argument to 174 | set top level options. 175 | 176 | ### Async! 177 | 178 | Please keep in mind that `usePackage` runs asynchronously, so that code invoked after `usePackage` 179 | calls is not guaranteed to (and almost certainly won't) run after the package is installed and 180 | configured. 181 | 182 | However, `usePackage` calls are performed in sequence, in invocation order, so that packages can 183 | assume previous packages have been fully installed, and code in a `usePackage`'s `init` hook is 184 | guaranteed to run after all previous `usePackage`s have fully completed. 185 | 186 | If you need to wait for the completion of a `usePackage` call, it returns a `Promise` that you 187 | can await the resolution of. Because `usePackage` calls run in order, if you need to wait for 188 | everything to fully complete, you can just wait for your last `usePackage` to complete. 189 | 190 | ## Licence 191 | 192 | Copyright 2019 Bodil Stokke 193 | 194 | This program is free software: you can redistribute it and/or modify it under the terms of the GNU 195 | Lesser General Public License as published by the Free Software Foundation, either version 3 of the 196 | License, or (at your option) any later version. 197 | 198 | This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without 199 | even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 200 | General Public License for more details. 201 | 202 | You should have received a copy of the GNU Lesser General Public License along with this program. If 203 | not, see https://www.gnu.org/licenses/. 204 | 205 | ## Code of Conduct 206 | 207 | Please note that this project is released with a [Contributor Code of Conduct][coc]. By 208 | participating in this project you agree to abide by its terms. 209 | 210 | [coc]: https://github.com/bodil/vscode-init-script/blob/master/CODE_OF_CONDUCT.md 211 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-use-package", 3 | "description": "Programmatic configuration for VS Code.", 4 | "version": "0.1.4", 5 | "licence": "LGPL-3.0+", 6 | "author": { 7 | "name": "Bodil Stokke", 8 | "url": "https://bodil.lol/" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/bodil/vscode-use-package" 13 | }, 14 | "main": "./out/main.js", 15 | "files": [ 16 | "/out/**" 17 | ], 18 | "scripts": { 19 | "prepublish": "npm run compile", 20 | "compile": "tsc -p ./", 21 | "watch": "tsc -watch -p ./", 22 | "pretest": "npm run compile", 23 | "test": "node ./out/test/runTest.js" 24 | }, 25 | "dependencies": { 26 | "@types/comment-json": "^1.1.1", 27 | "@types/node": "^14.0.13", 28 | "@types/vscode": "^1.46.0", 29 | "comment-json": "^2.4.2" 30 | }, 31 | "devDependencies": { 32 | "@typescript-eslint/eslint-plugin": "^3.3.0", 33 | "@typescript-eslint/parser": "^3.3.0", 34 | "eslint": "^7.2.0", 35 | "eslint-config-prettier": "^6.11.0", 36 | "prettier": "^2.0.5", 37 | "typescript": "^3.9.5" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | import * as Path from "path"; 3 | import * as Fs from "fs"; 4 | import * as Json from "comment-json"; 5 | 6 | export type Keybinding = { 7 | key: string; 8 | command: string; 9 | when?: string; 10 | args?: Record; 11 | }; 12 | 13 | export type UsePackageOptions = { 14 | local?: boolean; 15 | remote?: string | boolean; 16 | scope?: string; 17 | config?: Record; 18 | globalConfig?: Record; 19 | keymap?: Array; 20 | init?: () => Thenable; 21 | }; 22 | 23 | function readFile(path: string): Promise { 24 | return new Promise((resolve, reject) => 25 | Fs.readFile(path, { encoding: "utf-8" }, (err, result) => 26 | err ? reject(err) : resolve(result) 27 | ) 28 | ); 29 | } 30 | 31 | function writeFile(path: string, data: string): Promise { 32 | return new Promise((resolve, reject) => 33 | Fs.writeFile(path, data, { encoding: "utf-8" }, (err) => (err ? reject(err) : resolve())) 34 | ); 35 | } 36 | 37 | function defer(f: () => Thenable, delay?: number): Thenable { 38 | return new Promise((resolve, reject) => 39 | setTimeout(() => f().then(resolve, reject), delay || 0) 40 | ); 41 | } 42 | 43 | async function installExtension(name: string) { 44 | await vscode.commands.executeCommand("workbench.extensions.installExtension", name); 45 | return await new Promise((resolve, reject) => { 46 | let retries = 20; 47 | let delay = 0; 48 | function retry() { 49 | setTimeout(() => { 50 | if (!vscode.extensions.getExtension(name)) { 51 | if (retries > 0) { 52 | delay += 10; 53 | retries -= 1; 54 | retry(); 55 | } else { 56 | reject(new Error(`Failed to install extension: "${name}"`)); 57 | } 58 | } else { 59 | resolve(); 60 | } 61 | }, delay); 62 | } 63 | retry(); 64 | }); 65 | } 66 | 67 | type ProgressBar = { 68 | progress: vscode.Progress<{ increment: number; message: string }>; 69 | total: number; 70 | message?: string; 71 | report: (message: string) => void; 72 | increment: () => void; 73 | }; 74 | 75 | type QueueItem = { 76 | name: string; 77 | resolve: () => void; 78 | reject: (error?: any) => void; 79 | }; 80 | 81 | type Queue = { 82 | items: Array; 83 | idle: boolean; 84 | scheduled: number; 85 | failed: number; 86 | progress?: ProgressBar; 87 | }; 88 | 89 | const queue: Queue = { 90 | items: [], 91 | idle: true, 92 | progress: undefined, 93 | scheduled: 0, 94 | failed: 0, 95 | }; 96 | 97 | function buildProgressBar( 98 | progress: vscode.Progress<{ increment: number; message: string }> 99 | ): ProgressBar { 100 | return { 101 | progress, 102 | total: queue.scheduled, 103 | message: undefined, 104 | report: function (message) { 105 | this.progress.report({ increment: 0, message }); 106 | this.message = message; 107 | }, 108 | increment: function () { 109 | this.progress.report({ 110 | increment: 100 / this.total, 111 | message: this.message || "", 112 | }); 113 | }, 114 | }; 115 | } 116 | 117 | function startQueue(): Thenable { 118 | return defer(() => (queue.idle ? processQueue() : Promise.resolve())); 119 | } 120 | 121 | async function processQueue() { 122 | queue.idle = false; 123 | await vscode.window.withProgress( 124 | { 125 | cancellable: false, 126 | location: vscode.ProgressLocation.Notification, 127 | title: "Use-Package", 128 | }, 129 | async function ( 130 | progress: vscode.Progress<{ increment: number; message: string }>, 131 | _token: vscode.CancellationToken 132 | ) { 133 | queue.progress = buildProgressBar(progress); 134 | await processStep(); 135 | } 136 | ); 137 | } 138 | 139 | function pluralise(count: number): string { 140 | return count === 1 ? "" : "s"; 141 | } 142 | 143 | async function processStep() { 144 | if (queue.items.length === 0) { 145 | queue.idle = true; 146 | queue.progress = undefined; 147 | if (queue.scheduled > 0) { 148 | if (queue.failed > 0) { 149 | const installed = queue.scheduled - queue.failed; 150 | if (installed > 0) { 151 | vscode.window.showErrorMessage( 152 | `Installed ${installed} extension${pluralise(installed)}, and ${ 153 | queue.failed 154 | } extension${pluralise(queue.failed)} failed to install.` 155 | ); 156 | } else { 157 | vscode.window.showErrorMessage( 158 | `${queue.failed} extension${pluralise(queue.failed)} failed to install.` 159 | ); 160 | } 161 | } else { 162 | vscode.window.showInformationMessage( 163 | `Installed ${queue.scheduled} extension${pluralise(queue.scheduled)}.` 164 | ); 165 | } 166 | } 167 | queue.scheduled = 0; 168 | queue.failed = 0; 169 | return; 170 | } 171 | 172 | const next = queue.items.shift(); 173 | if (next === undefined || queue.progress === undefined) { 174 | throw new Error(); 175 | } 176 | queue.progress.report(`Installing extension "${next.name}"`); 177 | try { 178 | await installExtension(next.name); 179 | console.log("Installed successfully:", next.name); 180 | next.resolve(); 181 | } catch (err) { 182 | next.reject(err); 183 | queue.failed += 1; 184 | vscode.window.showErrorMessage(`${err}`); 185 | } 186 | await defer(processStep); 187 | } 188 | 189 | function addToQueue(name: string): Thenable { 190 | return new Promise((resolve, reject) => { 191 | queue.scheduled += 1; 192 | queue.items.push({ name, resolve, reject }); 193 | startQueue(); 194 | }); 195 | } 196 | 197 | async function install(name: string) { 198 | if (vscode.extensions.getExtension(name)) { 199 | return Promise.resolve(); 200 | } else { 201 | return addToQueue(name); 202 | } 203 | } 204 | 205 | function extensionName(name: string): string { 206 | const parts = name.split("."); 207 | return parts.pop() || ""; 208 | } 209 | 210 | let extensionContext: vscode.ExtensionContext | undefined = undefined; 211 | 212 | export function initUsePackage(context: vscode.ExtensionContext): void { 213 | extensionContext = context; 214 | } 215 | 216 | function getExtensionContext(): vscode.ExtensionContext { 217 | if (extensionContext === undefined) { 218 | const message = 219 | "You must initialise Use-Package by calling " + 220 | "`initUsePackage(context)` before you can use it!"; 221 | vscode.window.showErrorMessage(message); 222 | throw new Error(message); 223 | } else { 224 | return extensionContext; 225 | } 226 | } 227 | 228 | export async function usePackage(name: string, options?: UsePackageOptions): Promise { 229 | options = options || {}; 230 | // If `local` is true and we're running on a remote, do nothing. 231 | if (options.local === true && vscode.env.remoteName !== undefined) { 232 | return; 233 | } 234 | // If `remote` is true and we're not running on a remote, do nothing. 235 | if (options.remote === true && vscode.env.remoteName === undefined) { 236 | return; 237 | } 238 | // If `remote` is a string and we're not running on a remote matching that string, do nothing. 239 | if (typeof options.remote === "string" && options.remote !== vscode.env.remoteName) { 240 | return; 241 | } 242 | await install(name); 243 | const scope = options.scope || extensionName(name); 244 | if (options.config !== undefined) { 245 | await configSet(scope, options.config); 246 | } 247 | if (options.globalConfig !== undefined) { 248 | await configSet(undefined, options.globalConfig); 249 | } 250 | if (options.keymap !== undefined) { 251 | await keymapSet(options.keymap); 252 | } 253 | if (options.init !== undefined) { 254 | await options.init(); 255 | } 256 | } 257 | 258 | export async function configSet( 259 | scope: string | Record | undefined, 260 | options?: Record 261 | ): Promise { 262 | if (typeof scope === "object") { 263 | options = scope; 264 | scope = undefined; 265 | } 266 | if (options === undefined) { 267 | return; 268 | } 269 | const config = vscode.workspace.getConfiguration(scope); 270 | for (const key of Object.keys(options)) { 271 | const value = options[key]; 272 | const state = config.inspect(key); 273 | if (state === undefined || state.globalValue !== value) { 274 | await config.update(key, value, vscode.ConfigurationTarget.Global); 275 | } 276 | } 277 | } 278 | 279 | type KeyboardQueue = { 280 | items: Array; 281 | lock: boolean; 282 | }; 283 | 284 | const keyboardQueue: KeyboardQueue = { 285 | items: [], 286 | lock: false, 287 | }; 288 | 289 | function keyIndex(keymap: Array, key: Keybinding): number | undefined { 290 | for (let i = 0; i < keymap.length; i++) { 291 | if ( 292 | keymap[i] && 293 | keymap[i].key === key.key && 294 | keymap[i].when === key.when && 295 | keymap[i].command === key.command 296 | ) { 297 | return i; 298 | } 299 | } 300 | return undefined; 301 | } 302 | 303 | function setKey(keymap: Array, key: Keybinding) { 304 | const index = keyIndex(keymap, key); 305 | if (index === undefined) { 306 | keymap.push(key); 307 | } else { 308 | keymap[index] = key; 309 | } 310 | } 311 | 312 | async function keymapQueueRun() { 313 | if (keyboardQueue.lock) { 314 | return; 315 | } 316 | keyboardQueue.lock = true; 317 | // Delay write by 100ms to let the maximum number 318 | // of bindings queue up beforehand. 319 | await defer(() => Promise.resolve(), 100); 320 | const masterPath = Path.resolve( 321 | getExtensionContext().globalStoragePath, 322 | "../../keybindings.json" 323 | ); 324 | const originalData = await readFile(masterPath); 325 | const master = Json.parse(originalData); 326 | 327 | let key; 328 | while ((key = keyboardQueue.items.pop())) { 329 | setKey(master, key); 330 | } 331 | 332 | const masterData = Json.stringify(master, undefined, 4); 333 | if (masterData !== originalData) { 334 | await writeFile(masterPath, masterData); 335 | } 336 | keyboardQueue.lock = false; 337 | if (keyboardQueue.items.length > 0) { 338 | await keymapQueueRun(); 339 | } 340 | } 341 | 342 | export async function keymapSet(keymap: Array): Promise { 343 | for (const key of keymap) { 344 | keyboardQueue.items.push(key); 345 | } 346 | await keymapQueueRun(); 347 | } 348 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "target": "es6", 6 | "outDir": "out", 7 | "lib": ["es6"], 8 | "sourceMap": true, 9 | "rootDir": "src", 10 | "strict": true /* enable all strict type-checking options */ 11 | /* Additional Checks */ 12 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 13 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 14 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 15 | }, 16 | "exclude": ["node_modules", ".vscode-test", "out"] 17 | } 18 | --------------------------------------------------------------------------------