├── .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 |
--------------------------------------------------------------------------------