├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
├── settings.json
└── tasks.json
├── .vscodeignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── images
├── example.gif
├── logo.png
├── logo.svg
└── terminal.gif
├── package.json
├── src
├── command-processor.ts
├── command-variables.ts
├── extension.ts
├── file-ignore-checker.ts
├── run-on-save.ts
├── types.ts
└── util.ts
├── test
├── .vscode-test.mjs
├── fixture
│ └── .gitignore
├── src
│ └── extension.test.ts
└── tsconfig.json
└── tsconfig.json
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | /out
3 | /test/out
4 | .vscode-test/
5 | *.vsix
6 | *.tsbuildinfo
7 | package-lock.json
--------------------------------------------------------------------------------
/.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 | "eg2.tslint"
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 | "--disable-extensions",
15 | "--extensionDevelopmentPath=${workspaceFolder}"
16 | ],
17 | "outFiles": [
18 | "${workspaceFolder}/out/**/*.js"
19 | ],
20 | "preLaunchTask": "npm: watch"
21 | }
22 | ]
23 | }
24 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | .vscode-test/**
3 | test/**
4 | src/**
5 | .gitignore
6 | vsc-extension-quickstart.md
7 | **/tsconfig.json
8 | **/tslint.json
9 | **/*.tsbuildinfo
10 | **/*.map
11 | **/*.ts
12 | **/*.svg
13 | **/*.gif
14 |
15 | # under node_modules
16 | **/.github/**
17 | **/node_modules/**/.*
18 | **/node_modules/**/@types/**
19 | **/node_modules/**/test/**
20 | **/node_modules/**/*.md
21 | **/node_modules/**/*.markdown
22 | **/node_modules/**/LICENSE
23 | **/node_modules/**/license
24 | **/node_modules/**/README
25 | **/node_modules/**/readme
26 | **/node_modules/**/CHANGELOG
27 | **/node_modules/**/changelog
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # ChangeLog
2 |
3 | ## [1.10.1]
4 | - Fix #49, when `workingDirectoryAsCWD` is `true`, and multiple root folders existing, `cwd` can be rightly choose the root folder that recently saved file belongs to.
5 |
6 | ## [1.10.0]
7 | - Merges `strawhat-dev@github`'s pull request, supports `commands[].languages` to do language id matching.
8 |
9 | ## [1.9.2]
10 | - `defaultRunIn` option now it's default value is `backend`, not `vscode`. If your commands can't run, try specify this option.
11 |
12 | ## [1.9.1]
13 | - Fix #44, which will cause `defaultRunIn` option not working.
14 |
15 | ## [1.9.0]
16 | - Add a `doNotDisturb` option to prevent output tab from been focused on non-zero exit code. Thanks to pull request from `Tristano8@github` .
17 |
18 |
19 | ## [1.8.0]
20 | - Supports `onlyRunOnManualSave` option, to limit running commands only for manually save.
21 | - Fixes the wrong `commandBeforeSaving` usage description.
22 |
23 |
24 | ## [1.7.1]
25 |
26 | - Merges `CyprusSocialite@github`'s pull request, supports `clearOutput` option, and adjust plugin activation time.
27 | - Supports `commandBeforeSaving`, to specifies commands before saving document.
28 | - `globMatch` can be specified as a relative path, which will be used to match file path relative to current workspace directory.
29 |
30 |
31 | ## [1.7.0]
32 |
33 | - Merges `jackwhelpton@github`'s pull request, supports more variables.
34 | - Adjusts test settings.
35 |
36 |
37 | ## [1.6.0]
38 |
39 | - Adds a `terminalHideTimeout` option, to support close terminal automatically after command ran.
40 | - Adds a `ignoreFilesBy` option, to support ignore files that listed in like `.gitignore`.
41 | - Adds a `args` option, to provide arguments for command, especially for vscode command.
42 |
43 |
44 | ## [1.5.0]
45 |
46 | - Adds a `async` option, to support run commands in a sequence.
47 |
48 |
49 | ## [1.4.3]
50 |
51 | - `globMatch` will also apply "Variable Substitution", so you may specify a `globMatch` expression that include `${workspaceFolder}` to only match saved files in current workspace.
52 |
53 |
54 | ## [1.4.0]
55 |
56 | - Supports `forcePathSeparator` option.
57 |
58 |
59 | ## [1.3.1]
60 |
61 | - Supports `${fileRelative}`.
62 |
63 |
64 | ## [1.3.0]
65 |
66 | - Supports `commandBeforeSaving`.
67 | - Supports `globMatch` to make it easier to specifies file matcher.
68 |
69 |
70 | ## [1.2.0]
71 |
72 | - Supports vscode command, Thanks to Hulvdan@github.
73 |
74 |
75 | ## [1.1.0]
76 |
77 | - Will escape command line arguments automatically when file or directory name contains white space.
78 |
79 |
80 | ## [1.0.6]
81 |
82 | - Supports run command in terminal.
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 pucelle
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | Run on Save - VSCode Extension
4 |
5 |
6 | Configure shell commands and related file patterns, commands will be executed when matched files were saved.
7 |
8 |
9 | ## Features
10 |
11 | You can specify status bar messages which will show before and after commands executing, such that they will tell you what's happening and not distrub you much:
12 |
13 | 
14 |
15 | If you prefer running commands in vscode terminal, which keeps message colors and give more feedback details, change the `runIn` option to `terminal`.
16 |
17 | 
18 |
19 | If you need to run VS Code's commands change `runIn` option to `vscode`
20 |
21 |
22 | ## Configuration
23 |
24 | | Name | Description
25 | | --- | ---
26 | | `runOnSave.statusMessageTimeout` | Specify the default timeout milliseconds after which the status bar message will hide, default value is `3000`, means 3 seconds.
27 | | `runOnSave.ignoreFilesBy` | Specifies it to ignore files that list in `.gitignore` or `.npmignore`. default value is empty list.
28 | | `runOnSave.shell` | Specify in which shell the commands are executed, defaults to the default vscode shell.
29 | | `runOnSave.defaultRunIn` | Specify default `commands[].runIn` for all the commands, defaults to `backend` if not specified.
30 | | `runOnSave.onlyRunOnManualSave` | Whether to only run commands when a file is manually saved, defaults to `false`.
31 | | `runOnSave.commands` | Specify the array of commands to execute and related info, its child options as below.
32 |
33 | ### Command Options
34 |
35 | Note, `languages`, `match`, `globMatch` work like filter, if specifies more than one, command must match all then can run. So normally specifying one of them would be enough.
36 |
37 | | Name | Description
38 | | --- | ---
39 | | `commands[].languages` | Specify an array of language ids to filter commands, when language matches command will run.
40 | | `commands[].match` | Specify RegExp source string, files which's path match will be included. E.g.: `\\.scss$` can used to match scss files.
41 | | `commands[].notMatch` | Specify RegExp source string, files which's path match will be excluded even they were included by `match` or `globMatch`. E.g.: `[\\\\\\/]_[\\w-]+\\.scss$` can be used to exclude scss library files.
42 | | `commands[].globMatch` | Specify a glob expression, to match the whole file path or the relative path relative to current workspace directory. the matched files will be included. E.g.: `**/*.scss` will match all scss files, `*.scss` will match all scss files located in current workspace directory.
43 | | `commands[].command` | Specify the shell command to execute. You may include variable substitution like what to do in [VSCode Tasks](https://code.visualstudio.com/docs/editor/tasks#_variable-substitution).
44 | | `commands[].commandBeforeSaving` | Same as `commands`, but runs it before saving action happens, and document is not saved yet.
45 | | `commands[].args` | Specify the command parameters, can be a string, array of string, or an object.
46 | | `commands[].forcePathSeparator` | Force path separator in variable substitution to be `/`, `\\`, default is not specified.
47 | | `commands[].async` | All the commands with `async: false` will run in a sequence, means run next after previous completed. Default value is `true`. |
48 | | `commands[].runningStatusMessage` | Specify the status bar message when the shell command begin to run, supports variable substitution too. Only works when `runIn=backend`.
49 | | `commands[].finishStatusMessage` | Specify the status bar message after the shell command finished executing, also supports variable substitution. Only works when `runIn=backend`.
50 | | `commands[].statusMessageTimeout` | Specify the timeout milliseconds of current message, after which the status bar message will hide, default value is `3000`, means 3 seconds.
51 | | `commands[].terminalHideTimeout` | Specify the timeout in milliseconds after which the terminal for running current command will hide. Only works when `runIn=terminal`. If default value is `-1`, set it as a value `>=0` can make it work.
52 | | `commands[].workingDirectoryAsCWD`| Specify the vscode working directory as shell CWD (Current Working Directory). Only works when `runIn=backend`.
53 | | `commands[].clearOutput` | Clear the output channel before running current command. Default value is `false`.
54 | | `commands[].doNotDisturb` | By default, output tab would get focus after receiving non-zero exit codes. Set this option to `true` can prevent it. Only works when `runIn=backend`.
55 | | `commands[].runIn` | See list below. Default value is specified by `runOnSave.defaultRunIn`, or `vscode`.
56 | - `backend`: Run command silently and show messages in output channel, you can specify runningStatusMessage and finishStatusMessage to give you a little feedback. Choose this when you don't want to be disturbed.
57 | - `terminal`: Run command in vscode terminal, which keeps message colors. Choose this when you want to get feedback details.
58 | - `vscode`: Run vscode's command. Choose this if you want to execute vscode's own command or a command of a particular extension.
59 |
60 |
61 | ### Sample Configuration
62 |
63 | ```js
64 | {
65 | "runOnSave.statusMessageTimeout": 3000,
66 | "runOnSave.commands": [
67 | {
68 | // Match scss files except names start with `_`.
69 | "match": ".*\\.scss$",
70 | "notMatch": "[\\\\\\/]_[^\\\\\\/]*\\.scss$",
71 | "command": "node-sass ${file} ${fileDirname}/${fileBasenameNoExtension}.css",
72 | "runIn": "backend",
73 | "runningStatusMessage": "Compiling ${fileBasename}",
74 | "finishStatusMessage": "${fileBasename} compiled"
75 | },
76 | {
77 | // Match less files except names start with `_`.
78 | "globMatch": "**/[^_]*.less",
79 | "command": "node-sass ${file} ${fileDirname}/${fileBasenameNoExtension}.css",
80 | "runIn": "terminal"
81 | },
82 | {
83 | // Match any python files by path.
84 | "match": ".*\\.py$",
85 | "command": "python.runLinting",
86 | "runIn": "vscode"
87 | },
88 | {
89 | // Match any python files by language id.
90 | "languages": ["python"],
91 | "command": "python.runLinting",
92 | "runIn": "vscode"
93 | }
94 | ]
95 | }
96 | ```
97 |
98 |
99 | ### Variable Substitution
100 |
101 | Can be used in `command`, `runningStatusMessage`, `finishStatusMessage`, `globMatch`.
102 |
103 | Note that if `forcePathSeparator` specified, separators in these variables will be replaced.
104 |
105 | | Variable | Description
106 | | --- | ---
107 | | `${workspaceFolder}` | the path of the folder opened in VS Code.
108 | | `${workspaceFolderBasename}` | the name of the folder opened in VS Code without any slashes (/).
109 | | `${file}` | the path of current opened file.
110 | | `${fileBasename}` | the basename part of current opened file.
111 | | `${fileBasenameNoExtension}` | the basename part without extension of current opened file.
112 | | `${fileExtname}` | the extension part of current opened file.
113 | | `${fileRelative}` | the shorter relative file path part from current vscode working directory.
114 | | `${fileDirname}` | the dirname path part of current opened file.
115 | | `${fileDirnameBasename}` | the basename of dirname path part of current opened file.
116 | | `${fileDirnameRelative}` | the shorter relative dirname path part from current vscode working directory.
117 | | `${cwd}` | the task runner's current working directory on startup.
118 | | `${userHome}` | current user's system home directory.
119 | | `${lineNumber}` | number of active line in vscode editor.
120 | | `${selectedText}` | selected text in vscode editor.
121 | | `${execPath}` | absolute pathname of the vscode process.
122 | | `${defaultBuildTaskName}` | default build task name of current project.
123 | | `${pathSeparator}` | path separator based on system.
124 | | `${env:envName}` | reference environment variable `envName`.
125 | | `${config:vsConfigName}` | reference vscode configuration name `vsConfigName`.
126 | | `${command:vsCommandName}` | execute vscode command `vsCommandName`, and reference returned result.
127 |
128 |
129 | To better distinguish file path associated variables, assume you have opened a workspace located at `/Users/UserName/ProjectName`, and you are editing `folderName/subFolderName/fileName.css` inside of it, then:
130 |
131 | | Variable | Value
132 | | --- | ---
133 | | `${workspaceFolder}` | `/Users/UserName/ProjectName`
134 | | `${workspaceFolderBasename}` | `ProjectName`
135 | | `${file}` | `/Users/UserName/ProjectName/folderName/subFolderName/fileName.css`
136 | | `${fileBasename}` | `fileName.css`
137 | | `${fileBasenameNoExtension}` | `fileName`
138 | | `${fileExtname}` | `css`
139 | | `${fileRelative}` | `folderName/subFolderName/fileName.css`
140 | | `${fileDirname}` | `/Users/UserName/ProjectName/folderName/subFolderName`
141 | | `${fileDirnameBasename}` | `subFolderName`
142 | | `${fileDirnameRelative}` | `folderName/subFolderName`
143 |
144 |
145 |
146 | ## Commands
147 |
148 | The following commands are exposed in the command palette
149 |
150 | - `Run On Save: Enable` - to enable the extension
151 | - `Run On Save: Disable` - to disable the extension
152 |
153 |
154 | ## References
155 |
156 | This plugin inspired from these 2 plugins:
157 |
158 | [vscode-runonsave](https://github.com/emeraldwalk/vscode-runonsave) and [vscode-save-and-run](https://github.com/wk-j/vscode-save-and-run).
159 |
160 |
161 | ## License
162 |
163 | MIT
164 |
--------------------------------------------------------------------------------
/images/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pucelle/vscode-run-on-save/0c24eda6bcc7d94c0e8c06c9537ded2135cf0d3a/images/example.gif
--------------------------------------------------------------------------------
/images/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pucelle/vscode-run-on-save/0c24eda6bcc7d94c0e8c06c9537ded2135cf0d3a/images/logo.png
--------------------------------------------------------------------------------
/images/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
12 |
--------------------------------------------------------------------------------
/images/terminal.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/pucelle/vscode-run-on-save/0c24eda6bcc7d94c0e8c06c9537ded2135cf0d3a/images/terminal.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "run-on-save",
3 | "displayName": "Run on Save",
4 | "description": "Run configured shell commands when a file is saved in vscode, and output configured messages on status bar.",
5 | "icon": "images/logo.png",
6 | "version": "1.10.2",
7 | "license": "MIT",
8 | "publisher": "pucelle",
9 | "homepage": "https://github.com/pucelle/vscode-run-on-save",
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/pucelle/vscode-run-on-save"
13 | },
14 | "bugs": {
15 | "url": "https://github.com/pucelle/vscode-run-on-save/issues"
16 | },
17 | "engines": {
18 | "vscode": "^1.81.0"
19 | },
20 | "categories": [
21 | "Other"
22 | ],
23 | "keywords": [
24 | "run on save",
25 | "save and run",
26 | "compile scss on save",
27 | "compile less on save",
28 | "compile files on save"
29 | ],
30 | "activationEvents": [
31 | "onStartupFinished"
32 | ],
33 | "main": "./out/extension.js",
34 | "contributes": {
35 | "commands": [
36 | {
37 | "command": "extension.enableRunOnSave",
38 | "title": "Run On Save: Enable"
39 | },
40 | {
41 | "command": "extension.disableRunOnSave",
42 | "title": "Run On Save: Disable"
43 | }
44 | ],
45 | "configuration": {
46 | "title": "Run On Save",
47 | "type": "object",
48 | "properties": {
49 | "runOnSave.statusMessageTimeout": {
50 | "type": "number",
51 | "description": "Specify the timeout in millisecond after which the status message will be hidden. Works when `runIn=backend`, can be overwritten by the `statusMessageTimeout` in each command.",
52 | "default": 3000
53 | },
54 | "runOnSave.ignoreFilesBy": {
55 | "type": "array",
56 | "items": {
57 | "type": "string",
58 | "enum": [
59 | ".gitignore",
60 | ".npmignore"
61 | ]
62 | },
63 | "default": [],
64 | "description": "Specifies it to ignore files list in `.gitignore` or `.npmignore`."
65 | },
66 | "runOnSave.shell": {
67 | "type": "string",
68 | "description": "Specify what shell will be used for all the commands. Default is vscode's default shell.",
69 | "default": null
70 | },
71 | "runOnSave.defaultRunIn": {
72 | "type": "string",
73 | "description": "Run command in which environment by default. Default value is `backend`.",
74 | "enum": [
75 | "backend",
76 | "terminal",
77 | "vscode"
78 | ],
79 | "default": "backend",
80 | "enumDescriptions": [
81 | "Run command silently and output messages to output channel, you can specify `runningStatusMessage` and `finishStatusMessage` to give you feedback. Choose this when you don't want to be disturbed.",
82 | "Run command in vscode terminal, which keeps message colors. Choose this when you want to get command feedback details.",
83 | "Run vscode's command. Choose this if you want to execute vscode's own command or a command from a installed vscode extension."
84 | ]
85 | },
86 | "runOnSave.onlyRunOnManualSave": {
87 | "type": "boolean",
88 | "description": "Only run commands when manually saving a file. Default value is `false`.",
89 | "default": false
90 | },
91 | "runOnSave.commands": {
92 | "type": "array",
93 | "description": "Shell commands array.",
94 | "default": [],
95 | "items": {
96 | "type": "object",
97 | "properties": {
98 | "languages": {
99 | "type": "array",
100 | "description": "Specify an array of languageIds for which this command applies to.",
101 | "default": [],
102 | "uniqueItems": true,
103 | "items": {
104 | "type": "string",
105 | "examples": [
106 | "abap",
107 | "bat",
108 | "bibtex",
109 | "clojure",
110 | "coffeescript",
111 | "c",
112 | "cpp",
113 | "csharp",
114 | "dockercompose",
115 | "css",
116 | "cuda-cpp",
117 | "d",
118 | "dart",
119 | "pascal",
120 | "diff",
121 | "dockerfile",
122 | "erlang",
123 | "fsharp",
124 | "git-rebase",
125 | "go",
126 | "groovy",
127 | "handlebars",
128 | "haml",
129 | "haskell",
130 | "html",
131 | "ini",
132 | "java",
133 | "javascript",
134 | "javascriptreact",
135 | "json",
136 | "jsonc",
137 | "julia",
138 | "latex",
139 | "less",
140 | "lua",
141 | "makefile",
142 | "markdown",
143 | "objective-c",
144 | "objective-cpp",
145 | "ocaml",
146 | "pascal",
147 | "perl6",
148 | "php",
149 | "plaintext",
150 | "powershell",
151 | "pug",
152 | "python",
153 | "r",
154 | "razor",
155 | "ruby",
156 | "rust",
157 | "syntax)",
158 | "shaderlab",
159 | "shellscript",
160 | "slim",
161 | "sql",
162 | "stylus",
163 | "svelte",
164 | "swift",
165 | "typescript",
166 | "typescriptreact",
167 | "tex",
168 | "vb",
169 | "vue",
170 | "vue-html",
171 | "xml",
172 | "xsl",
173 | "yaml"
174 | ]
175 | }
176 | },
177 | "match": {
178 | "type": "string",
179 | "description": "Specify a RegExp source to match file path. Note if specifies both `match` and `globMatch`, commands matched them both will be executed.",
180 | "default": ""
181 | },
182 | "notMatch": {
183 | "type": "string",
184 | "description": "Specify a RegExp source, the files whole path match it will be excluded.",
185 | "default": ""
186 | },
187 | "globMatch": {
188 | "type": "string",
189 | "description": "Specify a glob expression to match file path. reference to: https://github.com/isaacs/node-glob#glob-primer. Note if specifies both `match` and `globMatch`, only commands matched them both will be executed.",
190 | "default": ""
191 | },
192 | "commandBeforeSaving": {
193 | "type": "string",
194 | "description": "Specify the command to be executed before saving the file. Note that for backend command, file will be saved after command executed completed.",
195 | "default": "echo ${file}"
196 | },
197 | "command": {
198 | "type": "string",
199 | "description": "Specify the command to be executed after file saved.",
200 | "default": "echo ${file}"
201 | },
202 | "args": {
203 | "type": [
204 | "string",
205 | "array",
206 | "object"
207 | ],
208 | "description": "Specify the command parameters, can be a string, array of string, or an object. Note for a `backend` or `terminal` command, if args option is defined as array of string, or an object, will format argument to add quotes if needed. e.g., `['-a', 'Value 1']` will be formatted as `-a \"Value 1\"`",
209 | "default": ""
210 | },
211 | "runIn": {
212 | "type": "string",
213 | "description": "Run command in which environment, if not specified, uses `runOnSave.defaultRunIn`.",
214 | "enum": [
215 | "backend",
216 | "terminal",
217 | "vscode"
218 | ],
219 | "default": "backend",
220 | "enumDescriptions": [
221 | "Run command silently and output messages to output channel, you can specify `runningStatusMessage` and `finishStatusMessage` to give you feedback. Choose this when you don't want to be disturbed.",
222 | "Run command in vscode terminal, which keeps message colors. Choose this when you want to get command feedback details.",
223 | "Run vscode's command. Choose this if you want to execute vscode's own command or a command from a installed vscode extension."
224 | ]
225 | },
226 | "async": {
227 | "type": "boolean",
228 | "description": "All the commands with `async: false` will run in a sequence, means run next after previous completed. Default value is `true`.",
229 | "default": true
230 | },
231 | "runningStatusMessage": {
232 | "type": "string",
233 | "description": "Specify the status bar message when the shell command began to run. Only works when `runIn=backend`.",
234 | "default": ""
235 | },
236 | "finishStatusMessage": {
237 | "type": "string",
238 | "description": "Specify the status bar message after the shell command finished executing. Only works when `runIn=backend`.",
239 | "default": ""
240 | },
241 | "statusMessageTimeout": {
242 | "type": "number",
243 | "description": "Specify the timeout in millisecond after which the status message will hide. Only works when `runIn=backend`.",
244 | "default": 3000
245 | },
246 | "terminalHideTimeout": {
247 | "type": "number",
248 | "description": "Specify the timeout in millisecond after which the terminal for running current command will hide. Only works when `runIn=terminal`. If default value is `-1`, set it as a value `>=0` can make it work.",
249 | "default": -1
250 | },
251 | "workingDirectoryAsCWD": {
252 | "type": "boolean",
253 | "description": "Specify the vscode working directory as shell CWD (Current Working Directory). Only works when `runIn=backend`.",
254 | "default": true
255 | },
256 | "clearOutput": {
257 | "type": "boolean",
258 | "description": "Clear the output channel before running. Default value is `false`.",
259 | "default": false
260 | }
261 | }
262 | }
263 | },
264 | "runOnSave.commandsBeforeSaving": {
265 | "type": "array",
266 | "description": "Shell commands array, just like `runOnSave.commands`, but runs before saving documents.",
267 | "default": [],
268 | "items": {
269 | "type": "object",
270 | "properties": {
271 | "languages": {
272 | "type": "array",
273 | "description": "Specify an array of language ids for which this command applies to.",
274 | "default": [],
275 | "uniqueItems": true,
276 | "items": {
277 | "type": "string",
278 | "examples": [
279 | "abap",
280 | "bat",
281 | "bibtex",
282 | "clojure",
283 | "coffeescript",
284 | "c",
285 | "cpp",
286 | "csharp",
287 | "dockercompose",
288 | "css",
289 | "cuda-cpp",
290 | "d",
291 | "dart",
292 | "pascal",
293 | "diff",
294 | "dockerfile",
295 | "erlang",
296 | "fsharp",
297 | "git-rebase",
298 | "go",
299 | "groovy",
300 | "handlebars",
301 | "haml",
302 | "haskell",
303 | "html",
304 | "ini",
305 | "java",
306 | "javascript",
307 | "javascriptreact",
308 | "json",
309 | "jsonc",
310 | "julia",
311 | "latex",
312 | "less",
313 | "lua",
314 | "makefile",
315 | "markdown",
316 | "objective-c",
317 | "objective-cpp",
318 | "ocaml",
319 | "pascal",
320 | "perl6",
321 | "php",
322 | "plaintext",
323 | "powershell",
324 | "pug",
325 | "python",
326 | "r",
327 | "razor",
328 | "ruby",
329 | "rust",
330 | "syntax",
331 | "shaderlab",
332 | "shellscript",
333 | "slim",
334 | "sql",
335 | "stylus",
336 | "svelte",
337 | "swift",
338 | "typescript",
339 | "typescriptreact",
340 | "tex",
341 | "vb",
342 | "vue",
343 | "vue-html",
344 | "xml",
345 | "xsl",
346 | "yaml"
347 | ]
348 | }
349 | },
350 | "match": {
351 | "type": "string",
352 | "description": "Specify a RegExp source to match file path. Note if specifies both `match` and `globMatch`, commands matched them both will be executed.",
353 | "default": ""
354 | },
355 | "notMatch": {
356 | "type": "string",
357 | "description": "Specify a RegExp source, the files whole path match it will be excluded.",
358 | "default": ""
359 | },
360 | "globMatch": {
361 | "type": "string",
362 | "description": "Specify a glob expression to match file path. reference to: https://github.com/isaacs/node-glob#glob-primer. Note if specifies both `match` and `globMatch`, only commands matched them both will be executed.",
363 | "default": ""
364 | },
365 | "commandBeforeSaving": {
366 | "type": "string",
367 | "description": "Specify the command to be executed before saving the file. Note that for backend command, file will be saved after command executed completed.",
368 | "default": "echo ${file}"
369 | },
370 | "command": {
371 | "type": "string",
372 | "description": "Specify the command to be executed after file saved.",
373 | "default": "echo ${file}"
374 | },
375 | "args": {
376 | "type": [
377 | "string",
378 | "array",
379 | "object"
380 | ],
381 | "description": "Specify the command parameters, can be a string, array of string, or an object. Note for a `backend` or `terminal` command, if args option is defined as array of string, or an object, will format argument to add quotes if needed. e.g., `['-a', 'Value 1']` will be formatted as `-a \"Value 1\"`",
382 | "default": ""
383 | },
384 | "runIn": {
385 | "type": "string",
386 | "description": "Run command in.",
387 | "enum": [
388 | "backend",
389 | "terminal",
390 | "vscode"
391 | ],
392 | "default": "backend",
393 | "enumDescriptions": [
394 | "Run command silently and output messages to output channel, you can specify `runningStatusMessage` and `finishStatusMessage` to give you feedback. Choose this when you don't want to be disturbed.",
395 | "Run command in vscode terminal, which keeps message colors. Choose this when you want to get command feedback details.",
396 | "Run vscode's command. Choose this if you want to execute vscode's own command or a command from a installed vscode extension."
397 | ]
398 | },
399 | "async": {
400 | "type": "boolean",
401 | "description": "All the commands with `async: false` will run in a sequence, means run next after previous completed. Default value is `true`.",
402 | "default": true
403 | },
404 | "runningStatusMessage": {
405 | "type": "string",
406 | "description": "Specify the status bar message when the shell command began to run. Only works when `runIn=backend`.",
407 | "default": ""
408 | },
409 | "finishStatusMessage": {
410 | "type": "string",
411 | "description": "Specify the status bar message after the shell command finished executing. Only works when `runIn=backend`.",
412 | "default": ""
413 | },
414 | "statusMessageTimeout": {
415 | "type": "number",
416 | "description": "Specify the timeout in millisecond after which the status message will hide. Only works when `runIn=backend`.",
417 | "default": 3000
418 | },
419 | "terminalHideTimeout": {
420 | "type": "number",
421 | "description": "Specify the timeout in millisecond after which the terminal for running current command will hide. Only works when `runIn=terminal`. If default value is `-1`, set it as a value `>=0` can make it work.",
422 | "default": -1
423 | },
424 | "workingDirectoryAsCWD": {
425 | "type": "boolean",
426 | "description": "Specify the vscode working directory as shell CWD (Current Working Directory). Only works when `runIn=backend`.",
427 | "default": true
428 | },
429 | "clearOutput": {
430 | "type": "boolean",
431 | "description": "Clear the output channel before running. Default value is `false`.",
432 | "default": false
433 | },
434 | "doNotDisturb": {
435 | "type": "boolean",
436 | "description": "By default, output tab would get focus after receiving non-zero exit codes. Set this option to `true` can prevent it. Only works when `runIn=backend`.",
437 | "default": false
438 | }
439 | }
440 | }
441 | }
442 | }
443 | }
444 | },
445 | "scripts": {
446 | "vscode:prepublish": "npm run build",
447 | "build": "tsc -p ./",
448 | "watch": "tsc -watch -p ./",
449 | "test": "cd test && tsc -b && vscode-test"
450 | },
451 | "devDependencies": {
452 | "@types/fs-extra": "^11.0.1",
453 | "@types/minimatch": "^3.0.5",
454 | "@types/mocha": "^2.2.48",
455 | "@types/node": "^20.5.4",
456 | "@types/vscode": "^1.81.0",
457 | "@vscode/test-cli": "0.0.4",
458 | "@vscode/test-electron": "^2.3.8",
459 | "typescript": "^4.9.5"
460 | },
461 | "dependencies": {
462 | "fs-extra": "^11.1.1",
463 | "minimatch": "^3.1.2"
464 | }
465 | }
466 |
--------------------------------------------------------------------------------
/src/command-processor.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 | import {formatCommandPieces, encodeCommandLineToBeQuotedIf} from './util'
3 | import * as minimatch from 'minimatch'
4 | import {CommandVariables} from './command-variables'
5 | import * as path from 'path'
6 | import {Configuration, PathSeparator, RawCommand, VSCodeDocumentPartial} from './types'
7 |
8 |
9 | /** Processed command base, will be extended. */
10 | export interface ProcessedCommandBase {
11 | languages?: string[]
12 | match?: RegExp
13 | notMatch?: RegExp
14 | globMatch?: string
15 | commandBeforeSaving?: string
16 | command: string
17 | args?: string[] | object | string
18 | forcePathSeparator?: PathSeparator
19 | runIn: string
20 | async?: boolean
21 | clearOutput?: boolean
22 | doNotDisturb?: boolean
23 | }
24 |
25 | export interface BackendCommand extends ProcessedCommandBase {
26 | runIn: 'backend'
27 | runningStatusMessage: string
28 | finishStatusMessage: string
29 | workingDirectoryAsCWD: boolean
30 | statusMessageTimeout?: number
31 | doNotDisturb?: boolean
32 | }
33 |
34 | export interface TerminalCommand extends ProcessedCommandBase {
35 | runIn: 'terminal'
36 | statusMessageTimeout?: number
37 | terminalHideTimeout?: number
38 | }
39 |
40 | export interface VSCodeCommand extends ProcessedCommandBase {
41 | runIn: 'vscode'
42 | args?: string[] | object | string
43 | }
44 |
45 | export type ProcessedCommand = BackendCommand | TerminalCommand | VSCodeCommand
46 |
47 |
48 |
49 | export class CommandProcessor {
50 |
51 | private commands: ProcessedCommand[] = []
52 |
53 | setRawCommands(commands: RawCommand[], defaultRunIn: Configuration['defaultRunIn']) {
54 | this.commands = this.processCommands(commands, defaultRunIn)
55 | }
56 |
57 | private processCommands(commands: RawCommand[], defaultRunIn: Configuration['defaultRunIn']): ProcessedCommand[] {
58 | return commands.map(command => {
59 | return Object.assign({}, command, {
60 | runIn: command.runIn || defaultRunIn || 'backend',
61 | languages: command.languages,
62 | match: command.match ? new RegExp(command.match, 'i') : undefined,
63 | notMatch: command.notMatch ? new RegExp(command.notMatch, 'i') : undefined,
64 | globMatch: command.globMatch ? command.globMatch : undefined
65 | }) as ProcessedCommand
66 | })
67 | }
68 |
69 | /** Prepare raw commands to link current working file. */
70 | prepareCommandsForFileBeforeSaving(document: VSCodeDocumentPartial): Promise {
71 | return this.prepareCommandsForDocument(document, true)
72 | }
73 |
74 | /** Prepare raw commands to link current working file. */
75 | prepareCommandsForFileAfterSaving(document: VSCodeDocumentPartial): Promise {
76 | return this.prepareCommandsForDocument(document, false)
77 | }
78 |
79 | /** Prepare raw commands to link current working file. */
80 | private async prepareCommandsForDocument(document: VSCodeDocumentPartial, forCommandsAfterSaving: boolean): Promise {
81 | let preparedCommands: ProcessedCommand[] = []
82 |
83 | for (let command of await this.filterCommandsByDocument(document)) {
84 | let commandString = forCommandsAfterSaving
85 | ? command.commandBeforeSaving
86 | : command.command
87 |
88 | if (!commandString) {
89 | continue
90 | }
91 |
92 | let pathSeparator = command.forcePathSeparator
93 |
94 | if (command.runIn === 'backend') {
95 | preparedCommands.push({
96 | runIn: 'backend',
97 | command: this.formatArgs(await this.formatCommandString(commandString, pathSeparator, document.uri), command.args),
98 | runningStatusMessage: await this.formatVariables(command.runningStatusMessage, pathSeparator, document.uri),
99 | finishStatusMessage: await this.formatVariables(command.finishStatusMessage, pathSeparator, document.uri),
100 | async: command.async ?? true,
101 | clearOutput: command.clearOutput ?? false,
102 | doNotDisturb: command.doNotDisturb ?? false,
103 | } as BackendCommand)
104 | }
105 | else if (command.runIn === 'terminal') {
106 | preparedCommands.push({
107 | runIn: 'terminal',
108 | command: this.formatArgs(await this.formatCommandString(commandString, pathSeparator, document.uri), command.args),
109 | async: command.async ?? true,
110 | clearOutput: command.clearOutput ?? false,
111 | } as TerminalCommand)
112 | }
113 | else {
114 | preparedCommands.push({
115 | runIn: 'vscode',
116 | command: await this.formatCommandString(commandString, pathSeparator, document.uri),
117 | args: command.args,
118 | async: command.async ?? true,
119 | clearOutput: command.clearOutput ?? false,
120 | } as VSCodeCommand)
121 | }
122 | }
123 |
124 | return preparedCommands
125 | }
126 |
127 | private async filterCommandsByDocument(document: VSCodeDocumentPartial): Promise {
128 | let filteredCommands = []
129 |
130 | for (let command of this.commands) {
131 | let {languages, match, notMatch, globMatch} = command
132 |
133 | if (!this.doLanguageTest(languages, document)) {
134 | continue
135 | }
136 |
137 | if (!this.doMatchTest(match, notMatch, document.uri)) {
138 | continue
139 | }
140 |
141 | if (!(await this.doGlobMatchTest(globMatch, document.uri))) {
142 | continue
143 | }
144 |
145 | filteredCommands.push(command)
146 | }
147 |
148 | return filteredCommands
149 | }
150 |
151 | private doLanguageTest(languages: string[] | undefined, document: VSCodeDocumentPartial): boolean {
152 |
153 | // No languages specified, not filter out document.
154 | if (!languages?.length) {
155 | return true
156 | }
157 |
158 | // Does not apply if user has specified languages and the current document is a `NotebookDocument`.
159 | if (!('languageId' in document)) {
160 | return false
161 | }
162 |
163 | // Match `languageId` case-insensitively.
164 | return languages.some((languageId) => languageId.toLowerCase() === document.languageId!.toLowerCase())
165 | }
166 |
167 | private doMatchTest(match: RegExp | undefined, notMatch: RegExp | undefined, uri: vscode.Uri): boolean {
168 | if (match && !match.test(uri.fsPath)) {
169 | return false
170 | }
171 |
172 | if (notMatch && notMatch.test(uri.fsPath)) {
173 | return false
174 | }
175 |
176 | return true
177 | }
178 |
179 | private async doGlobMatchTest(globMatch: string | undefined, uri: vscode.Uri): Promise {
180 | if (!globMatch) {
181 | return true
182 | }
183 |
184 | if (/\${(?:\w+:)?[\w\.]+}/.test(globMatch)) {
185 | globMatch = await this.formatVariables(globMatch, undefined, uri)
186 | }
187 |
188 | let gm = new minimatch.Minimatch(globMatch)
189 |
190 | // If match whole path.
191 | if (gm.match(uri.fsPath)) {
192 | return true
193 | }
194 |
195 | // Or match relative path.
196 | let relativePath = path.relative(vscode.workspace.getWorkspaceFolder(uri)?.uri.fsPath || '', uri.fsPath)
197 | if (gm.match(relativePath)) {
198 | return true
199 | }
200 |
201 | return false
202 | }
203 |
204 | private async formatCommandString(command: string, pathSeparator: PathSeparator | undefined, uri: vscode.Uri): Promise {
205 | if (!command) {
206 | return ''
207 | }
208 |
209 | // If white spaces exist in file name or directory name, we need to wrap them with `""`.
210 | // We do this by testing each piece, and wrap them if needed.
211 | return formatCommandPieces(command, async (piece) => {
212 | return CommandVariables.format(piece, uri, pathSeparator)
213 | })
214 | }
215 |
216 | private async formatVariables(message: string, pathSeparator: PathSeparator | undefined, uri: vscode.Uri): Promise {
217 | if (!message) {
218 | return ''
219 | }
220 |
221 | return CommandVariables.format(message, uri, pathSeparator)
222 | }
223 |
224 | /** Add args to a command string. */
225 | private formatArgs(command: string, args: string[] | object | string | undefined): string {
226 | if (!args) {
227 | return command
228 | }
229 |
230 | if (Array.isArray(args)) {
231 | for (let arg of args) {
232 | command += ' ' + encodeCommandLineToBeQuotedIf(arg)
233 | }
234 | }
235 | else if (typeof args === 'string') {
236 | command += ' ' + args
237 | }
238 | else if (typeof args === 'object') {
239 | for (let [key, value] of Object.entries(args)) {
240 | command += ' ' + key + ' ' + encodeCommandLineToBeQuotedIf(value)
241 | }
242 | }
243 |
244 | return command
245 | }
246 | }
247 |
--------------------------------------------------------------------------------
/src/command-variables.ts:
--------------------------------------------------------------------------------
1 | import * as path from 'path'
2 | import * as vscode from 'vscode'
3 | import {formatPathSeparator, replaceAsync} from './util'
4 | import {homedir} from 'os'
5 |
6 |
7 | type VariableProvider = (uri: vscode.Uri) => string | Promise
8 | type SubVariableProvider = (name: string, uri: vscode.Uri) => string | Promise
9 |
10 |
11 | export namespace CommandVariables {
12 |
13 | const VariableProviders: Record = {
14 | userHome: () => homedir(),
15 | workspaceFolder: (uri) => getRootPath(uri),
16 | workspaceFolderBasename: (uri) => path.basename(getRootPath(uri)),
17 | file: (uri) => uri.fsPath,
18 | fileBasename: (uri) => path.basename(uri.fsPath),
19 | fileBasenameNoExtension: (uri) => path.basename(uri.fsPath, path.extname(uri.fsPath)),
20 | fileExtname: (uri) => path.extname(uri.fsPath),
21 | fileRelative: (uri) => path.relative(getRootPath(uri), uri.fsPath),
22 | fileDirname: (uri) => getDirName(uri.fsPath),
23 | fileDirnameBasename: (uri) => path.basename(getDirName(uri.fsPath)),
24 | fileDirnameRelative: (uri) => getDirName(path.relative(getRootPath(uri), uri.fsPath)),
25 |
26 | cwd: () => process.cwd(),
27 | lineNumber: () => getEditor()?.selection.active.line.toString() || '',
28 | selectedText: () => getEditor()?.document.getText(getEditor()!.selection) || '',
29 | execPath: () => process.execPath,
30 | defaultBuildTaskName: defaultBuildTaskName,
31 | pathSeparator: () => path.sep,
32 | }
33 |
34 | const SubVariableProviders: Record = {
35 | env: name => process.env[name] || '',
36 | config: (name, uri) => vscode.workspace.getConfiguration('', uri)?.get(name) || '',
37 | command: async name => await vscode.commands.executeCommand(name),
38 | }
39 |
40 | // `path.dirname(...)` can't be used to handle paths like `\\dir\name`.
41 | function getDirName(filePath: string): string {
42 | return filePath.replace(/[\\\/][^\\\/]+$/, '') || filePath[0] || ''
43 | }
44 |
45 | function getRootPath(uri: vscode.Uri): string {
46 | return vscode.workspace.getWorkspaceFolder(uri)?.uri.fsPath || ''
47 | }
48 |
49 | function getEditor() {
50 | return vscode.window.activeTextEditor
51 | }
52 |
53 | async function defaultBuildTaskName() {
54 | let tasks = await vscode.tasks.fetchTasks()
55 | let task = tasks.find(t => t.group?.id == vscode.TaskGroup.Build.id && t.group.isDefault)
56 |
57 | return task?.name || ''
58 | }
59 |
60 |
61 | /** Format variables of a string in kind of command / command piece / message. */
62 | export async function format(string: string, uri: vscode.Uri, pathSeparator: string | undefined) {
63 |
64 | // Compatible with old versioned syntax `${env.xxx}`.
65 | string = string.replace(/\$\{env\.(\w+)\}/g, '${env:$1}')
66 |
67 | return await replaceAsync(string, /\$\{(?:(\w+):)?([\w\.]+)\}/g, async (m0: string, prefix: string | undefined, name: string) => {
68 | let value = await getVariableValue(prefix, name, uri)
69 | if (value !== null) {
70 | return formatPathSeparator(value, pathSeparator)
71 | }
72 |
73 | return m0
74 | })
75 | }
76 |
77 | /** Get each variable value by RegExp match result. */
78 | async function getVariableValue(prefix: string | undefined, name: string, uri: vscode.Uri): Promise {
79 | if (prefix && SubVariableProviders.hasOwnProperty(prefix)) {
80 | return SubVariableProviders[prefix](name, uri)
81 | }
82 | else if (VariableProviders.hasOwnProperty(name)) {
83 | return VariableProviders[name](uri)
84 | }
85 | else {
86 | return null
87 | }
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode';
2 | import {RunOnSaveExtension} from './run-on-save';
3 |
4 |
5 | export function activate(context: vscode.ExtensionContext): RunOnSaveExtension {
6 | const extension = new RunOnSaveExtension(context)
7 |
8 | context.subscriptions.push(
9 | vscode.workspace.onDidChangeConfiguration(() => {
10 | extension.loadConfig()
11 | }),
12 |
13 | vscode.commands.registerCommand('extension.enableRunOnSave', () => {
14 | extension.setEnabled(true)
15 | }),
16 |
17 | vscode.commands.registerCommand('extension.disableRunOnSave', () => {
18 | extension.setEnabled(false)
19 | }),
20 |
21 | vscode.workspace.onWillSaveTextDocument((e: vscode.TextDocumentWillSaveEvent) => {
22 | e.waitUntil(extension.onWillSaveDocument(e.document, e.reason))
23 | }),
24 |
25 | vscode.workspace.onWillSaveNotebookDocument((e: vscode.NotebookDocumentWillSaveEvent) => {
26 | e.waitUntil(extension.onWillSaveDocument(e.notebook, e.reason))
27 | }),
28 |
29 | vscode.workspace.onDidSaveTextDocument((document: vscode.TextDocument) => {
30 | extension.onDocumentSaved(document)
31 | }),
32 |
33 | vscode.workspace.onDidSaveNotebookDocument((document: vscode.NotebookDocument) => {
34 | extension.onDocumentSaved(document)
35 | }),
36 | )
37 |
38 | return extension
39 | }
40 |
--------------------------------------------------------------------------------
/src/file-ignore-checker.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs-extra'
2 | import * as path from 'path'
3 | import {IMinimatch, Minimatch} from 'minimatch'
4 |
5 |
6 | export interface FileIgnoreCheckerOptions {
7 |
8 | /** Current workspace directory. */
9 | workspaceDir?: string
10 |
11 | /** Ignores file names. */
12 | ignoreFilesBy: string[]
13 | }
14 |
15 |
16 | export class FileIgnoreChecker {
17 |
18 | /** Current workspace directory, default value is `undefined`. */
19 | private workspaceDir: string | undefined
20 |
21 | /** Ignores file names. */
22 | private ignoreFilesBy: string[]
23 |
24 | constructor(options: FileIgnoreCheckerOptions) {
25 | this.workspaceDir = options.workspaceDir
26 | this.ignoreFilesBy = options.ignoreFilesBy
27 | }
28 |
29 | /** Test whether should ignore a file by it's path. */
30 | async shouldIgnore(filePath: string): Promise {
31 |
32 | // No ignore files should follow, never ignore.
33 | if (this.ignoreFilesBy.length === 0) {
34 | return false
35 | }
36 |
37 | let workspaceDir = this.workspaceDir
38 |
39 | // Not in current working directory, never ignore.
40 | if (workspaceDir && !path.normalize(filePath).startsWith(workspaceDir)) {
41 | return false
42 | }
43 |
44 | let dir = path.dirname(filePath)
45 | while (dir && dir !== workspaceDir) {
46 | for (let ignoreFileName of this.ignoreFilesBy) {
47 | let ignoreFilePath = path.join(dir, ignoreFileName)
48 |
49 | if (await fs.pathExists(ignoreFilePath)) {
50 | let shouldIgnore = await this.shouldIgnoreByIgnoreFilePath(filePath, ignoreFilePath)
51 | if (shouldIgnore) {
52 | return true
53 | }
54 | }
55 | }
56 |
57 | dir = path.dirname(dir)
58 | }
59 |
60 | return false
61 | }
62 |
63 | private async shouldIgnoreByIgnoreFilePath(filePath: string, ignoreFilePath: string): Promise {
64 | let ignoreRules = await this.parseIgnoreRules(ignoreFilePath)
65 | let relPath = path.relative(path.dirname(ignoreFilePath), filePath)
66 |
67 | return this.matchIgnoreRules(relPath, ignoreRules)
68 | }
69 |
70 | private async parseIgnoreRules(ignoreFilePath: string): Promise {
71 | let text = await fs.readFile(ignoreFilePath, 'utf8')
72 |
73 | let globOptions = {
74 | matchBase: true,
75 | dot: true,
76 | flipNegate: true,
77 | nocase: true
78 | }
79 |
80 | let ruleLines = text.split(/\r?\n/)
81 | .filter(line => !/^#|^$/.test(line.trim()))
82 |
83 | // Here it doesn't supports expressions like `!XXX`.
84 | let rules = ruleLines.map(pattern => {
85 | if (pattern.startsWith('/')) {
86 | pattern = pattern.slice(1)
87 | }
88 | else {
89 | pattern = '{**/,}' + pattern
90 | }
91 |
92 | if (pattern.endsWith('/')) {
93 | pattern = pattern.replace(/\/$/, '{/**,}')
94 | }
95 |
96 | return new Minimatch(pattern, globOptions)
97 | })
98 |
99 | return rules
100 | }
101 |
102 | private matchIgnoreRules(relPath: string, ignoreRules: IMinimatch[]): boolean {
103 | for (let rule of ignoreRules) {
104 | if (rule.match(relPath)) {
105 | return true
106 | }
107 | }
108 |
109 | return false
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/src/run-on-save.ts:
--------------------------------------------------------------------------------
1 | import {exec, ChildProcess} from 'child_process'
2 | import * as vscode from 'vscode'
3 | import {CommandProcessor, BackendCommand, TerminalCommand, VSCodeCommand, ProcessedCommand} from './command-processor'
4 | import {FleetingDoubleKeysCache, timeout} from './util'
5 | import {FileIgnoreChecker} from './file-ignore-checker'
6 | import {RawCommand, VSCodeDocument} from './types'
7 |
8 |
9 | export class RunOnSaveExtension {
10 |
11 | private context: vscode.ExtensionContext
12 | private config!: vscode.WorkspaceConfiguration
13 | private channel: vscode.OutputChannel = vscode.window.createOutputChannel('Run on Save')
14 | private commandProcessor: CommandProcessor = new CommandProcessor()
15 |
16 | /** A record of document uris and document versions to save reasons. */
17 | private documentSaveReasonCache: FleetingDoubleKeysCache
18 | = new FleetingDoubleKeysCache()
19 |
20 | constructor(context: vscode.ExtensionContext) {
21 | this.context = context
22 | this.loadConfig()
23 | this.showEnablingChannelMessage()
24 |
25 | context.subscriptions.push(this.channel)
26 | }
27 |
28 | /** Load or reload configuration. */
29 | loadConfig() {
30 | this.config = vscode.workspace.getConfiguration('runOnSave')
31 | this.commandProcessor.setRawCommands(this.config.get('commands') || [], this.config.get('defaultRunIn')!)
32 | }
33 |
34 | private showEnablingChannelMessage () {
35 | let message = `Run on Save is ${this.getEnabled() ? 'enabled' : 'disabled'}`
36 | this.showChannelMessage(message)
37 | this.showStatusMessage(message)
38 | }
39 |
40 | private showChannelMessage(message: string) {
41 | this.channel.appendLine(message)
42 | }
43 |
44 | getEnabled(): boolean {
45 | return !!this.context.globalState.get('enabled', true)
46 | }
47 |
48 | setEnabled(enabled: boolean) {
49 | this.context.globalState.update('enabled', enabled)
50 | this.showEnablingChannelMessage()
51 | }
52 |
53 | private showStatusMessage(message: string, timeout?: number) {
54 | timeout = timeout || this.config.get('statusMessageTimeout') || 3000
55 |
56 | let disposable = vscode.window.setStatusBarMessage(message, timeout)
57 | this.context.subscriptions.push(disposable)
58 | }
59 |
60 | /** Returns a promise it was resolved firstly and then will save document. */
61 | async onWillSaveDocument(document: VSCodeDocument, reason: vscode.TextDocumentSaveReason) {
62 | this.documentSaveReasonCache.set(document.uri.fsPath, document.version, reason)
63 |
64 | if (!this.getEnabled() || await this.shouldIgnore(document.uri, reason)) {
65 | return
66 | }
67 |
68 | let commandsToRun = await this.commandProcessor.prepareCommandsForFileBeforeSaving(document)
69 | if (commandsToRun.length > 0) {
70 | await this.runCommands(commandsToRun, document.uri)
71 | }
72 | }
73 |
74 | async onDocumentSaved(document: VSCodeDocument) {
75 | let reason = this.documentSaveReasonCache.get(document.uri.fsPath, document.version)
76 |
77 | if (!this.getEnabled() || await this.shouldIgnore(document.uri, reason)) {
78 | return
79 | }
80 |
81 | let commandsToRun = await this.commandProcessor.prepareCommandsForFileAfterSaving(document)
82 | if (commandsToRun.length > 0) {
83 | await this.runCommands(commandsToRun, document.uri)
84 | }
85 | }
86 |
87 | private async shouldIgnore(uri: vscode.Uri, reason: vscode.TextDocumentSaveReason | undefined): Promise {
88 | if (reason !== vscode.TextDocumentSaveReason.Manual && this.config.get('onlyRunOnManualSave')) {
89 | return true
90 | }
91 |
92 | let checker = new FileIgnoreChecker({
93 | workspaceDir: vscode.workspace.getWorkspaceFolder(uri)?.uri.fsPath,
94 | ignoreFilesBy: this.config.get('ignoreFilesBy') || [],
95 | })
96 |
97 | return checker.shouldIgnore(uri.fsPath)
98 | }
99 |
100 | private async runCommands(commands: ProcessedCommand[], uri: vscode.Uri) {
101 | let promises: Promise[] = []
102 | let syncCommands = commands.filter(c => !c.async)
103 | let asyncCommands = commands.filter(c => c.async)
104 |
105 | // Run commands in a parallel.
106 | for (let command of asyncCommands) {
107 | promises.push(this.runACommand(command, uri))
108 | }
109 |
110 | // Run commands in series.
111 | for (let command of syncCommands) {
112 | await this.runACommand(command, uri)
113 | }
114 |
115 | await Promise.all(promises)
116 | }
117 |
118 | private runACommand(command: ProcessedCommand, uri: vscode.Uri): Promise {
119 | if (command.clearOutput) {
120 | this.channel.clear()
121 | }
122 |
123 | let runIn = command.runIn || this.config.get('defaultRunIn') || 'backend'
124 |
125 | if (runIn === 'backend') {
126 | return this.runBackendCommand(command as BackendCommand, uri)
127 | }
128 | else if (runIn === 'terminal') {
129 | return this.runTerminalCommand(command as TerminalCommand)
130 | }
131 | else {
132 | return this.runVSCodeCommand(command as VSCodeCommand)
133 | }
134 | }
135 |
136 | private runBackendCommand(command: BackendCommand, uri: vscode.Uri) {
137 | return new Promise((resolve) => {
138 | this.showChannelMessage(`Running "${command.command}"`)
139 |
140 | if (command.runningStatusMessage) {
141 | this.showStatusMessage(command.runningStatusMessage, command.statusMessageTimeout)
142 | }
143 |
144 | let child = this.execShellCommand(command.command, command.workingDirectoryAsCWD ?? true, uri)
145 | child.stdout!.on('data', data => this.channel.append(data.toString()))
146 | child.stderr!.on('data', data => this.channel.append(data.toString()))
147 |
148 | child.on('exit', (e) => {
149 | if (e === 0 && command.finishStatusMessage) {
150 | this.showStatusMessage(command.finishStatusMessage, command.statusMessageTimeout)
151 | }
152 |
153 | if (e !== 0 && !command.doNotDisturb) {
154 | this.channel.show(true)
155 | }
156 |
157 | resolve()
158 | })
159 | }) as Promise
160 | }
161 |
162 | private execShellCommand(command: string, workingDirectoryAsCWD: boolean, uri: vscode.Uri): ChildProcess {
163 | let cwd: string | undefined
164 |
165 | if (workingDirectoryAsCWD) {
166 | cwd = vscode.workspace.getWorkspaceFolder(uri)?.uri.fsPath
167 | ?? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath
168 | }
169 |
170 | let shell = this.getShellPath()
171 |
172 | return shell ? exec(command, { shell, cwd }) : exec(command, { cwd })
173 | }
174 |
175 | private getShellPath(): string | undefined {
176 | return this.config.get('shell') || undefined
177 | }
178 |
179 | private async runTerminalCommand(command: TerminalCommand) {
180 | let terminal = this.createTerminal()
181 |
182 | terminal.show()
183 | terminal.sendText(command.command)
184 |
185 | await timeout(100)
186 | await vscode.commands.executeCommand("workbench.action.focusActiveEditorGroup")
187 |
188 | if ((command.terminalHideTimeout || -1) >= 0) {
189 | await timeout(command.terminalHideTimeout!)
190 | terminal.dispose()
191 | }
192 | }
193 |
194 | private createTerminal(): vscode.Terminal {
195 | let terminalName = 'Run on Save'
196 | let terminal = vscode.window.terminals.find(terminal => terminal.name === terminalName)
197 |
198 | if (!terminal) {
199 | this.context.subscriptions.push(terminal = vscode.window.createTerminal(terminalName, this.getShellPath()))
200 | }
201 |
202 | return terminal
203 | }
204 |
205 | private async runVSCodeCommand(command: VSCodeCommand) {
206 | // `finishStatusMessage` has to be hooked to exit of command execution.
207 | this.showChannelMessage(`Running "${command.command}"`)
208 |
209 | let args = this.formatVSCodeCommandArgs(command.args)
210 | await vscode.commands.executeCommand(command.command, ...args)
211 | }
212 |
213 | private formatVSCodeCommandArgs(args: string | object | string[] | undefined): any[] {
214 | if (Array.isArray(args)) {
215 | return args
216 | }
217 |
218 | if (['string', 'object'].includes(typeof args)) {
219 | return [args]
220 | }
221 |
222 | return []
223 | }
224 | }
225 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | import * as vscode from 'vscode'
2 |
3 |
4 | export interface Configuration {
5 | statusMessageTimeout: number
6 | ignoreFilesBy: string[]
7 | shell: String
8 | commands: RawCommand
9 | defaultRunIn: 'backend' | 'terminal' | 'vscode'
10 | }
11 |
12 | /** Raw command configured by user. */
13 | export interface RawCommand {
14 | languages?: string[]
15 | match?: string
16 | notMatch?: string
17 | globMatch?: string
18 | commandBeforeSaving?: string
19 | command?: string
20 | args?: string[] | object | string
21 | forcePathSeparator?: PathSeparator
22 | runIn?: string
23 | runningStatusMessage?: string
24 | finishStatusMessage?: string
25 | async?: boolean
26 | clearOutput?: boolean
27 | doNotDisturb?: boolean
28 | }
29 |
30 | export type PathSeparator = '/' | '\\'
31 |
32 | export type VSCodeDocument = vscode.TextDocument | vscode.NotebookDocument
33 |
34 | export type VSCodeDocumentPartial = Pick & Partial>
35 |
--------------------------------------------------------------------------------
/src/util.ts:
--------------------------------------------------------------------------------
1 | /** Format command string, quote piece part which includes white spaces. */
2 | export async function formatCommandPieces(command: string, formatPiece: (piece: string) => Promise): Promise {
3 | if (!command) {
4 | return ''
5 | }
6 |
7 | // If white spaces exist in file name or directory name, we need to wrap them with `""`.
8 | // We do this by testing each pieces, and wrap them if needed.
9 | return replaceAsync(command, /\S+/g, async (piece: string) => {
10 | let oldPiece = piece
11 | let alreadyQuoted = false
12 |
13 | if (piece[0] === '"' && piece[piece.length - 1] === '"') {
14 | piece = decodeQuotedCommandLine(piece.slice(1, -1))
15 | alreadyQuoted = true
16 | }
17 |
18 | // May need to be quoted after piece formatted.
19 | piece = await formatPiece(piece)
20 |
21 | // If piece includes spaces or `\\`, or be quoted before, then it must be encoded.
22 | if (piece !== oldPiece && /[\s"]|\\\\/.test(piece) || alreadyQuoted) {
23 | piece = '"' + encodeCommandLineToBeQuoted(piece) + '"'
24 | }
25 |
26 | return piece
27 | })
28 | }
29 |
30 | /** Encode `"` to `\"`. */
31 | export function encodeCommandLineToBeQuoted(command: string) {
32 | return command.replace(/["]/g, '\\$&')
33 | }
34 |
35 | /** If piece includes spaces, `\\`, or is quoted, then it must be encoded. */
36 | export function encodeCommandLineToBeQuotedIf(arg: string) {
37 | if (/[\s"]|\\\\/.test(arg)) {
38 | arg = '"' + encodeCommandLineToBeQuoted(arg) + '"'
39 | }
40 |
41 | return arg
42 | }
43 |
44 | /** Decode `\"` to `"`. */
45 | export function decodeQuotedCommandLine(command: string) {
46 | return command.replace(/\\(.)/g, '$1')
47 | }
48 |
49 |
50 | /** Replace path separators. */
51 | export function formatPathSeparator(path: string, pathSeparator: string | undefined) {
52 | return pathSeparator ? path.replace(/[\\|\/]/g, pathSeparator) : path
53 | }
54 |
55 |
56 | /** Resolves the returned promise after `ms` millseconds. */
57 | export function timeout(ms: number): Promise {
58 | return new Promise(resolve => setTimeout(resolve, ms))
59 | }
60 |
61 |
62 | /** Do RegExp replacing asynchronously. */
63 | export async function replaceAsync(str: string, re: RegExp, replacer: (...matches: string[]) => Promise): Promise {
64 | let replacements = await Promise.all(
65 | Array.from(str.matchAll(re),
66 | match => replacer(...match))
67 | )
68 |
69 | let i = 0
70 | return str.replace(re, () => replacements[i++])
71 | }
72 |
73 |
74 | /** All cache values exist for more than 3 seconds, but less than 6 seconds. */
75 | export class FleetingDoubleKeysCache {
76 |
77 | private timeoutMs: number
78 | private mapCurr: DoubleKeysMap = new DoubleKeysMap()
79 | private mapBack: DoubleKeysMap = new DoubleKeysMap()
80 | private timeout: any | null = null
81 |
82 | constructor(timeoutMs: number = 3000) {
83 | this.timeoutMs = timeoutMs
84 | }
85 |
86 | get(k1: K1, k2: K2): V | undefined {
87 | return this.mapCurr.get(k1, k2)
88 | ?? this.mapBack.get(k1, k2)
89 | }
90 |
91 | set(k1: K1, k2: K2, v: V) {
92 | this.mapCurr.set(k1, k2, v)
93 | this.setSwapTimeout()
94 | }
95 |
96 | firstKeyCount(): number {
97 | return this.mapCurr.firstKeyCount() + this.mapBack.firstKeyCount()
98 | }
99 |
100 | clear() {
101 | this.mapCurr.clear()
102 | this.mapBack.clear()
103 | }
104 |
105 | private setSwapTimeout() {
106 | if (this.timeout === null) {
107 | this.timeout = setTimeout(this.onSwapTimeout.bind(this), this.timeoutMs)
108 | }
109 | }
110 |
111 | private onSwapTimeout() {
112 | [this.mapCurr, this.mapBack] = [this.mapBack, this.mapCurr]
113 | this.mapCurr.clear()
114 | this.timeout = null
115 |
116 | // Need to swap a more time if has any values cached.
117 | if (this.firstKeyCount() > 0) {
118 | this.setSwapTimeout()
119 | }
120 | }
121 | }
122 |
123 |
124 | /**
125 | * `K1 -> K2 -> V` Map Struct.
126 | * Index each value by a pair of keys.
127 | */
128 | class DoubleKeysMap {
129 |
130 | private map: Map> = new Map()
131 |
132 | /** Has associated value by key pair. */
133 | has(k1: K1, k2: K2): boolean {
134 | let sub = this.map.get(k1)
135 | if (!sub) {
136 | return false
137 | }
138 |
139 | return sub.has(k2)
140 | }
141 |
142 | /** Get the count of all the first keys. */
143 | firstKeyCount(): number {
144 | return this.map.size
145 | }
146 |
147 | /** Get associated value by key pair. */
148 | get(k1: K1, k2: K2): V | undefined {
149 | let sub = this.map.get(k1)
150 | if (!sub) {
151 | return undefined
152 | }
153 |
154 | return sub.get(k2)
155 | }
156 |
157 | /** Set key pair and associated value. */
158 | set(k1: K1, k2: K2, v: V) {
159 | let sub = this.map.get(k1)
160 | if (!sub) {
161 | sub = new Map()
162 | this.map.set(k1, sub)
163 | }
164 |
165 | sub.set(k2, v)
166 | }
167 |
168 | /** Delete all the associated values by key pair. */
169 | delete(k1: K1, k2: K2) {
170 | let sub = this.map.get(k1)
171 | if (sub) {
172 | sub.delete(k2)
173 |
174 | if (sub.size === 0) {
175 | this.map.delete(k1)
176 | }
177 | }
178 | }
179 |
180 | /** Clear all the data. */
181 | clear() {
182 | this.map = new Map()
183 | }
184 | }
--------------------------------------------------------------------------------
/test/.vscode-test.mjs:
--------------------------------------------------------------------------------
1 | import { defineConfig } from '@vscode/test-cli';
2 |
3 | export default defineConfig({
4 | files: 'out/**/*.test.js',
5 | });
--------------------------------------------------------------------------------
/test/fixture/.gitignore:
--------------------------------------------------------------------------------
1 | should-ignore/
2 | should-ignore.css
--------------------------------------------------------------------------------
/test/src/extension.test.ts:
--------------------------------------------------------------------------------
1 | import * as assert from 'assert'
2 | import * as path from 'path'
3 | import * as vscode from 'vscode'
4 | import {CommandProcessor} from '../../out/command-processor'
5 | import {FileIgnoreChecker} from '../../out/file-ignore-checker'
6 | import {FleetingDoubleKeysCache} from '../../out/util'
7 | import {RawCommand, VSCodeDocumentPartial} from '../../out/types'
8 |
9 |
10 | suite("Extension Tests", () => {
11 | suite('test backend command', function () {
12 | let manager = new CommandProcessor()
13 | manager.setRawCommands([{
14 | 'match': '.*\\.scss$',
15 | 'notMatch': '[\\\\\\/]_[^\\\\\\/]*\\.scss$',
16 | 'runIn': 'backend',
17 | 'command': 'node-sass ${file} ${fileDirname}/${fileBasenameNoExtension}.css',
18 | 'runningStatusMessage': 'Compiling ${fileBasename}',
19 | 'finishStatusMessage': '${fileBasename} compiled',
20 | 'forcePathSeparator': '/',
21 | }], 'backend')
22 |
23 | test('will compile scss file in backend', async function () {
24 | let doc: VSCodeDocumentPartial = {
25 | uri: vscode.Uri.file('C:/folderName/fileName.scss')
26 | }
27 |
28 | let commands = await manager.prepareCommandsForFileAfterSaving(doc)
29 | assert.deepStrictEqual(commands, [{
30 | 'runIn': 'backend',
31 | 'command': 'node-sass c:/folderName/fileName.scss c:/folderName/fileName.css',
32 | 'runningStatusMessage': 'Compiling fileName.scss',
33 | 'finishStatusMessage': 'fileName.scss compiled',
34 | 'async': true,
35 | 'clearOutput': false,
36 | 'doNotDisturb': false,
37 | }])
38 | })
39 |
40 | test('will exclude scss file that file name starts with "_"', async function () {
41 | let doc: VSCodeDocumentPartial = {
42 | uri: vscode.Uri.file('C:/folderName/_fileName.scss')
43 | }
44 |
45 | let commands = await manager.prepareCommandsForFileAfterSaving(doc)
46 | assert.deepStrictEqual(commands, [])
47 | })
48 |
49 | test('will escape white spaces', async function () {
50 | let doc: VSCodeDocumentPartial = {
51 | uri: vscode.Uri.file('C:/folderName/fileName 1.scss')
52 | }
53 |
54 | let commands = await manager.prepareCommandsForFileAfterSaving(doc)
55 | assert.deepStrictEqual(
56 | commands[0].command,
57 | 'node-sass "c:/folderName/fileName 1.scss" "c:/folderName/fileName 1.css"'
58 | )
59 | })
60 | })
61 |
62 |
63 | suite('test globMatch', function () {
64 | let manager = new CommandProcessor()
65 | manager.setRawCommands([{
66 | 'globMatch': '**/*.scss',
67 | 'runIn': 'backend',
68 | 'command': 'node-sass ${file} ${fileDirname}/${fileBasenameNoExtension}.css',
69 | 'runningStatusMessage': 'Compiling ${fileBasename}',
70 | 'finishStatusMessage': '${fileBasename} compiled',
71 | 'async': true,
72 | 'forcePathSeparator': '/',
73 | }], 'backend')
74 |
75 | test('will compile scss file in backend', async function () {
76 | let doc: VSCodeDocumentPartial = {
77 | uri: vscode.Uri.file('C:/folderName/fileName.scss')
78 | }
79 |
80 | let commands = await manager.prepareCommandsForFileAfterSaving(doc)
81 | assert.deepStrictEqual(commands, [{
82 | 'runIn': 'backend',
83 | 'command': 'node-sass c:/folderName/fileName.scss c:/folderName/fileName.css',
84 | 'runningStatusMessage': 'Compiling fileName.scss',
85 | 'finishStatusMessage': 'fileName.scss compiled',
86 | 'async': true,
87 | 'clearOutput': false,
88 | 'doNotDisturb': false,
89 | }])
90 | })
91 | })
92 |
93 |
94 | suite('test commandBeforeSaving', function () {
95 | let manager = new CommandProcessor()
96 | manager.setRawCommands([{
97 | 'globMatch': '**/*.scss',
98 | 'runIn': 'backend',
99 | 'commandBeforeSaving': 'node-sass ${file} ${fileDirname}/${fileBasenameNoExtension}.css',
100 | 'runningStatusMessage': 'Compiling ${fileBasename}',
101 | 'finishStatusMessage': '${fileBasename} compiled',
102 | 'async': true,
103 | 'forcePathSeparator': '/',
104 | }], 'backend')
105 |
106 | test('will compile scss file in backend', async function () {
107 | let doc: VSCodeDocumentPartial = {
108 | uri: vscode.Uri.file('C:/folderName/fileName.scss')
109 | }
110 |
111 | let commands = await manager.prepareCommandsForFileBeforeSaving(doc)
112 | assert.deepStrictEqual(commands, [{
113 | 'runIn': 'backend',
114 | 'command': 'node-sass c:/folderName/fileName.scss c:/folderName/fileName.css',
115 | 'runningStatusMessage': 'Compiling fileName.scss',
116 | 'finishStatusMessage': 'fileName.scss compiled',
117 | 'async': true,
118 | 'clearOutput': false,
119 | 'doNotDisturb': false,
120 | }])
121 | })
122 | })
123 |
124 |
125 | suite('test backend command with back slash path', function () {
126 | let manager = new CommandProcessor()
127 | manager.setRawCommands([{
128 | 'match': '.*\\.scss$',
129 | 'notMatch': '[\\\\\\/]_[^\\\\\\/]*\\.scss$',
130 | 'runIn': 'backend',
131 | 'command': 'node-sass ${file} ${fileDirname}\\${fileBasenameNoExtension}.css',
132 | 'runningStatusMessage': 'Compiling ${fileBasename}',
133 | 'finishStatusMessage': '${fileBasename} compiled',
134 | }], 'backend')
135 |
136 | test('will escape paths starts with "\\\\"', async function () {
137 | let doc: VSCodeDocumentPartial = {
138 | uri: vscode.Uri.file('\\\\folderName\\fileName 1.scss')
139 | }
140 |
141 | let commands = await manager.prepareCommandsForFileAfterSaving(doc)
142 | assert.deepStrictEqual(
143 | commands[0].command,
144 | 'node-sass "\\\\folderName\\fileName 1.scss" "\\\\folderName\\fileName 1.css"'
145 | )
146 | })
147 | })
148 |
149 |
150 | suite('test terminal command', function () {
151 | let manager = new CommandProcessor()
152 | manager.setRawCommands([{
153 | 'match': '.*\\.scss$',
154 | 'notMatch': '[\\\\\\/]_[^\\\\\\/]*\\.scss$',
155 | 'runIn': 'terminal',
156 | 'command': 'node-sass ${file} ${fileDirname}/${fileBasenameNoExtension}.css',
157 | 'forcePathSeparator': '/',
158 | }], 'backend')
159 |
160 | test('will compile scss file in terminal', async function () {
161 | let doc: VSCodeDocumentPartial = {
162 | uri: vscode.Uri.file('C:/folderName/fileName.scss')
163 | }
164 |
165 | let commands = await manager.prepareCommandsForFileAfterSaving(doc)
166 | assert.deepStrictEqual(commands, [{
167 | 'runIn': 'terminal',
168 | 'command': 'node-sass c:/folderName/fileName.scss c:/folderName/fileName.css',
169 | 'async': true,
170 | 'clearOutput': false,
171 | }])
172 | })
173 | })
174 |
175 |
176 | suite('test for #20', function () {
177 | let manager = new CommandProcessor()
178 | manager.setRawCommands([{
179 | "match": ".*\\.drawio$",
180 | "runIn": "backend",
181 | "command": "draw.io --crop --export -f pdf \"${file}\"",
182 | 'forcePathSeparator': '/',
183 | }], 'backend')
184 |
185 | test('will compile it right', async function () {
186 | let doc: VSCodeDocumentPartial = {
187 | uri: vscode.Uri.file('C:/test.drawio')
188 | }
189 |
190 | let commands = await manager.prepareCommandsForFileAfterSaving(doc)
191 | assert.deepStrictEqual(commands, [{
192 | 'runIn': 'backend',
193 | 'command': 'draw.io --crop --export -f pdf "c:/test.drawio"',
194 | 'finishStatusMessage': '',
195 | 'runningStatusMessage': '',
196 | 'async': true,
197 | 'clearOutput': false,
198 | 'doNotDisturb': false,
199 | }])
200 | })
201 | })
202 |
203 |
204 | suite('test #47, supports `commands[].languages`', function () {
205 | let manager = new CommandProcessor()
206 | manager.setRawCommands([{
207 | "languages": ["typescript"],
208 | "runIn": "backend",
209 | "command": "anyCommandsToRun",
210 | }], 'backend')
211 |
212 | test('will compile it right', async function () {
213 | let doc: VSCodeDocumentPartial = {
214 | uri: vscode.Uri.file('C:/anyFileName'),
215 | languageId: 'typescript',
216 | }
217 |
218 | let commands = await manager.prepareCommandsForFileAfterSaving(doc)
219 | assert.deepStrictEqual(commands, [{
220 | 'runIn': 'backend',
221 | 'command': 'anyCommandsToRun',
222 | 'async': true,
223 | 'clearOutput': false,
224 | 'doNotDisturb': false,
225 | "finishStatusMessage": "",
226 | "runningStatusMessage": ""
227 | }])
228 | })
229 | })
230 |
231 |
232 | suite('test for #24, should ignore files follow ".gitignore"', function () {
233 | let checker = new FileIgnoreChecker({
234 | workspaceDir: path.resolve(__dirname, '../../'),
235 | ignoreFilesBy: ['.gitignore'],
236 | })
237 |
238 | test('will ignore file 1', async () => {
239 | assert.ok(await checker.shouldIgnore(path.resolve(__dirname, '../fixture/should-ignore/test.css')))
240 | })
241 |
242 | test('will ignore file 2', async () => {
243 | assert.ok(await checker.shouldIgnore(path.resolve(__dirname, '../fixture/should-ignore.css')))
244 | })
245 |
246 | test('will not ignore file 3', async () => {
247 | assert.ok(!await checker.shouldIgnore(path.resolve(__dirname, 'index.ts')))
248 | })
249 | })
250 |
251 |
252 | suite('test for #40, class FleetingDoubleKeysCache', function () {
253 | let cache = new FleetingDoubleKeysCache(100)
254 |
255 | test('will cache item', async () => {
256 | cache.set('a', 1, 'value')
257 | assert.equal(cache.get('a', 1), 'value')
258 | cache.clear()
259 | })
260 |
261 | test('will cache item for a while', async () => {
262 | cache.set('a', 1, 'value')
263 | await new Promise(resolve => setTimeout(resolve, 100))
264 | assert.equal(cache.get('a', 1), 'value')
265 | await new Promise(resolve => setTimeout(resolve, 50))
266 | assert.equal(cache.get('a', 1), 'value')
267 | cache.clear()
268 | })
269 |
270 | test('will clear after enough time', async () => {
271 | cache.set('a', 1, 'value')
272 | await new Promise(resolve => setTimeout(resolve, 250))
273 | assert.equal(cache.get('a', 1), undefined)
274 | cache.clear()
275 | })
276 | })
277 | })
--------------------------------------------------------------------------------
/test/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es2018",
5 | "outDir": "out",
6 | "lib": [
7 | "es2018"
8 | ],
9 | "sourceMap": true,
10 | "rootDir": "src",
11 | "strict": true,
12 | "noUnusedLocals": true,
13 | "noUnusedParameters": true,
14 | "noImplicitReturns": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "noImplicitAny": true,
17 | "noImplicitThis": true
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es2018",
5 | "outDir": "out",
6 | "lib": [
7 | "es2018"
8 | ],
9 | "sourceMap": true,
10 | "rootDir": "src",
11 | "strict": true,
12 | "noUnusedLocals": true,
13 | "noUnusedParameters": true,
14 | "noImplicitReturns": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "noImplicitAny": true,
17 | "noImplicitThis": true,
18 | "declaration": true,
19 | "composite": true
20 | },
21 | "exclude": [
22 | "node_modules",
23 | ".vscode-test",
24 | "out",
25 | "test"
26 | ]
27 | }
28 |
--------------------------------------------------------------------------------