├── .gitignore
├── images
├── icon.png
├── folding.gif
├── formatting.gif
├── issue-3.1.png
├── issue-3.2.png
├── highlighting.gif
└── icon.svg
├── .vscodeignore
├── .vscode
├── extensions.json
├── tasks.json
├── settings.json
└── launch.json
├── CHANGELOG.md
├── .editorconfig
├── .eslintrc.json
├── language-configuration.json
├── tsconfig.json
├── .github
└── workflows
│ ├── build.yml
│ └── release.yml
├── LICENSE
├── package.json
├── README.md
├── src
└── extension.ts
└── syntaxes
└── env.tmLanguage.json
/.gitignore:
--------------------------------------------------------------------------------
1 | out
2 | test
3 | node_modules
4 | *.vsix
5 |
--------------------------------------------------------------------------------
/images/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IronGeek/vscode-env/HEAD/images/icon.png
--------------------------------------------------------------------------------
/images/folding.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IronGeek/vscode-env/HEAD/images/folding.gif
--------------------------------------------------------------------------------
/images/formatting.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IronGeek/vscode-env/HEAD/images/formatting.gif
--------------------------------------------------------------------------------
/images/issue-3.1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IronGeek/vscode-env/HEAD/images/issue-3.1.png
--------------------------------------------------------------------------------
/images/issue-3.2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IronGeek/vscode-env/HEAD/images/issue-3.2.png
--------------------------------------------------------------------------------
/images/highlighting.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/IronGeek/vscode-env/HEAD/images/highlighting.gif
--------------------------------------------------------------------------------
/.vscodeignore:
--------------------------------------------------------------------------------
1 | .vscode/**
2 | src/**
3 | test/**
4 | .editorconfig
5 | .gitignore
6 | **/tsconfig.json
7 | **/.eslintrc.json
8 | **/*.map
9 | **/*.ts
10 |
--------------------------------------------------------------------------------
/.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 | "dbaeumer.vscode-eslint"
6 | ]
7 | }
8 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## [0.1.0] | 2020-10-13
4 |
5 | ### Features
6 |
7 | ↳ [`53e6022`](https://github.com/IronGeek/vscode-env/commit/53e6022cb0fbea99d3e6a678cd610904c18f548d) initial commit
8 |
9 | [0.1.0]: https://github.com/IronGeek/vscode-env/releases/tag/v0.1.0 "v0.1.0"
10 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | indent_style = space
5 | indent_size = 2
6 | charset = utf-8
7 | trim_trailing_whitespace = true
8 | insert_final_newline = true
9 |
10 | [*.md]
11 | indent_style = space
12 | indent_size = 2
13 | charset = utf-8
14 | trim_trailing_whitespace = false
15 | insert_final_newline = true
16 |
--------------------------------------------------------------------------------
/.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 |
--------------------------------------------------------------------------------
/.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 | }
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "root": true,
3 | "parser": "@typescript-eslint/parser",
4 | "parserOptions": {
5 | "ecmaVersion": 6,
6 | "sourceType": "module"
7 | },
8 | "plugins": [
9 | "@typescript-eslint"
10 | ],
11 | "rules": {
12 | "@typescript-eslint/naming-convention": "warn",
13 | "@typescript-eslint/semi": "warn",
14 | "curly": "warn",
15 | "eqeqeq": "warn",
16 | "no-throw-literal": "warn",
17 | "semi": "off"
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/language-configuration.json:
--------------------------------------------------------------------------------
1 | {
2 | "comments": {
3 | "lineComment": "#"
4 | },
5 | "brackets": [
6 | ["{", "}"],
7 | ["[", "]"],
8 | ["(", ")"]
9 | ],
10 | "autoClosingPairs": [
11 | ["{", "}"],
12 | ["[", "]"],
13 | ["(", ")"],
14 | ["\"", "\""],
15 | ["'", "'"]
16 | ],
17 | "surroundingPairs": [
18 | ["{", "}"],
19 | ["[", "]"],
20 | ["(", ")"],
21 | ["\"", "\""],
22 | ["'", "'"]
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "target": "es6",
5 | "outDir": "out",
6 | "lib": [
7 | "es6"
8 | ],
9 | "sourceMap": true,
10 | "rootDir": "src",
11 | "strict": true,
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": [
17 | "node_modules"
18 | ]
19 | }
20 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Build
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 | tags:
8 | - v*
9 |
10 | jobs:
11 | build:
12 | name: Build
13 | strategy:
14 | matrix:
15 | os: [macos-latest, ubuntu-latest, windows-latest]
16 | runs-on: ${{ matrix.os }}
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v2
20 | - name: Install Node.js
21 | uses: actions/setup-node@v1
22 | with:
23 | node-version: 12.x
24 | - name: Compile
25 | run: |
26 | npm ci
27 | npm run build
28 |
--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
1 | name: Release
2 |
3 | on:
4 | release:
5 | types:
6 | - published
7 | jobs:
8 | publish:
9 | name: Publish
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout
13 | uses: actions/checkout@v2
14 | - name: Install Node.js
15 | uses: actions/setup-node@v1
16 | with:
17 | node-version: 12.x
18 | - name: Compile
19 | run: |
20 | npm ci
21 | npm run build
22 | - name: Deploy
23 | if: success() && startsWith( github.ref, 'refs/tags/v')
24 | run: npm run deploy
25 | env:
26 | VSCE_PAT: ${{ secrets.VSCE_PAT }}
27 |
--------------------------------------------------------------------------------
/.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 | "args": [
13 | "--extensionDevelopmentPath=${workspaceFolder}"
14 | ],
15 | "outFiles": [
16 | "${workspaceFolder}/out/**/*.js"
17 | ],
18 | "preLaunchTask": "${defaultBuildTask}"
19 | }
20 | ]
21 | }
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Jakka Prihatna
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.
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vscode-env",
3 | "displayName": "ENV",
4 | "description": "Adds formatting and syntax highlighting support for env files (.env) to Visual Studio Code",
5 | "version": "0.1.0",
6 | "publisher": "IronGeek",
7 | "license": "MIT",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/IronGeek/vscode-env.git"
11 | },
12 | "bugs": {
13 | "url": "https://github.com/IronGeek/vscode-env/issues"
14 | },
15 | "homepage": "https://github.com/IronGeek/vscode-env/blob/master/README.md",
16 | "engines": {
17 | "vscode": "^1.50.0"
18 | },
19 | "categories": [
20 | "Programming Languages",
21 | "Formatters",
22 | "Other"
23 | ],
24 | "keywords": [
25 | "env",
26 | "dotenv"
27 | ],
28 | "icon": "images/icon.png",
29 | "galleryBanner": {
30 | "color": "#ECD53F",
31 | "theme": "light"
32 | },
33 | "activationEvents": [
34 | "onLanguage:env"
35 | ],
36 | "main": "./out/extension.js",
37 | "contributes": {
38 | "languages": [
39 | {
40 | "id": "env",
41 | "aliases": [
42 | "Environment Variables",
43 | "dotenv"
44 | ],
45 | "extensions": [
46 | ".env",
47 | ".env.sample",
48 | ".env.example"
49 | ],
50 | "configuration": "./language-configuration.json"
51 | }
52 | ],
53 | "grammars": [
54 | {
55 | "language": "env",
56 | "scopeName": "source.env",
57 | "path": "./syntaxes/env.tmLanguage.json"
58 | }
59 | ]
60 | },
61 | "scripts": {
62 | "vscode:prepublish": "npm run build",
63 | "compile": "tsc -p ./",
64 | "lint": "eslint src --ext ts",
65 | "watch": "tsc -watch -p ./",
66 | "build": "npm run compile && npm run lint",
67 | "deploy": "vsce publish"
68 | },
69 | "husky": {
70 | "hooks": {
71 | "pre-commit": "npm run build"
72 | }
73 | },
74 | "devDependencies": {
75 | "@types/node": "^14.11.8",
76 | "@types/vscode": "^1.50.0",
77 | "@typescript-eslint/eslint-plugin": "^4.4.1",
78 | "@typescript-eslint/parser": "^4.4.1",
79 | "eslint": "^7.11.0",
80 | "husky": "^4.3.0",
81 | "typescript": "^4.0.2",
82 | "vsce": "^1.81.1"
83 | }
84 | }
85 |
--------------------------------------------------------------------------------
/images/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # ENV
2 |
3 | 
4 | 
5 | 
6 | 
7 | 
8 |
9 | Adds formatting and syntax highlighting support for env files (`.env`) to Visual Studio Code
10 |
11 | ## Features
12 |
13 | - Syntax highlighting
14 |
15 | 
16 |
17 | - Folding
18 |
19 | The extension will enable folding on file content that are wrapped with the following pattern:
20 |
21 | ```text
22 | # ... << begin with comment(s)
23 | ...
24 | ... << folded content
25 | ...
26 | << end with a blank line
27 | ```
28 |
29 | 
30 |
31 | - Formatting
32 |
33 | Use the `Format Document` command (CTRL+SHIFT+I) from the `Command Pallete` (CTRL+SHIFT+P) to format the current env file
34 |
35 | 
36 |
37 | ## Custom env file extension
38 |
39 | The extension support env files with the following name:
40 |
41 | - `.env`
42 | - `.env.sample`
43 | - `.env.example`
44 |
45 | To enable support for other env files with specific naming convention/ file extension, use the `files.associations` settings in Visual Studio Code.
46 |
47 | For example, the following settings will enable support for `*.env.development` and `*.env.production` files:
48 |
49 | ```json
50 | "files.associations": {
51 | "*.env.development": "env",
52 | "*.env.production": "env"
53 | }
54 | ```
55 |
56 | ## Known Issues
57 |
58 | - Highlighting/ Formatting/ Folding doesn't work
59 |
60 | Other extensions (like [shell-format](https://marketplace.visualstudio.com/items?itemName=foxundermoon.shell-format)) could also provides contributions to env files (`.env`). When two or more extensions providing contributions to a same file, there's a chance the contributions of previous extension will be overwritten by the later (see: [github.com/microsoft/vscode-docs/issues/2862](https://github.com/microsoft/vscode-docs/issues/2862#issuecomment-599994967)).
61 |
62 | To workaround this issue, use Visual Studio Code `files.associations` to force the language of `.env` files to be always specified as `env`:
63 |
64 | 
65 |
66 | Other non permanent solution is by using the `Select Language Mode` button on the Visual Studio Code status bar and set the language to `Environment Variables` (alias of `env`) on the opened file:
67 |
68 | 
69 |
70 |
71 | ## Acknowledgements
72 |
73 | - [Mike Stead](https://github.com/mikestead) for [dotenv extension for vscode](https://github.com/mikestead/vscode-dotenv)
74 |
75 | ## License
76 |
77 | This project is licensed under the terms of the [MIT license](LICENSE).
78 |
--------------------------------------------------------------------------------
/src/extension.ts:
--------------------------------------------------------------------------------
1 | import { window, languages, OutputChannel, ExtensionContext,
2 | TextDocument, TextEdit, Position, Range,
3 | FoldingRange, FoldingRangeKind } from 'vscode';
4 |
5 | export class Logger {
6 | constructor(readonly output: OutputChannel) {
7 | this.output = output;
8 | }
9 |
10 | log(msg: string): void {
11 | const d = new Date();
12 | const date = d.toISOString().split('T')[0];
13 | const time = d.toTimeString().split(' ')[0];
14 | const ms = (d.getMilliseconds() + '').padStart(3, '0');
15 |
16 | this.output.appendLine(`[${date} ${time}.${ms}] ${msg}`);
17 | }
18 |
19 | dispose(): void {
20 | this.output.dispose();
21 | }
22 | }
23 |
24 | export function activate(context: ExtensionContext) {
25 | const logger = new Logger(window.createOutputChannel('ENV'));
26 | logger.log('activating extension');
27 |
28 | const formatEditProvider = languages.registerDocumentFormattingEditProvider('env', {
29 | provideDocumentFormattingEdits(document: TextDocument): TextEdit[] {
30 | logger.log(`formatting ${document.fileName}`);
31 |
32 | let edits: TextEdit[] = [];
33 |
34 | for (let i = 0; i < document.lineCount; i++) {
35 | const ln = document.lineAt(i);
36 | const st = ln.range.start;
37 | const tx = ln.text;
38 |
39 | if (ln.isEmptyOrWhitespace) {
40 | if (tx.length > 0) {
41 | edits.push(TextEdit.delete(ln.range));
42 | }
43 | continue;
44 | }
45 |
46 | const fi = ln.firstNonWhitespaceCharacterIndex;
47 | const fs = new Position(i, fi);
48 | if (fi > 0) { // remove leading whitespace
49 | edits.push(TextEdit.delete(new Range(st, fs)));
50 | }
51 |
52 | if (tx.charAt(fi) === '#') { // remove trailing whitespace in comments
53 | edits.push(TextEdit.replace(new Range(fs, ln.range.end), '# ' + tx.substring(fi+1).trim()));
54 | } else if (tx.substr(fi, 6) === 'export') { // remove whitespace between export keywords
55 | let ex = tx.substring(fi+7).trim();
56 | let fe = ex.indexOf('=');
57 | if (fe > 0) {
58 | let key = ex.substring(0, fe).trim();
59 | let val = ex.substring(fe+1).trim();
60 |
61 | if (val.indexOf(' ') >= 0 && (val[0] !== '"' && val[val.length-1] !== '"')) {
62 | val = `"${val}"`;
63 | }
64 | edits.push(TextEdit.replace(new Range(fs, ln.range.end), 'export ' + key + '=' + val));
65 | } else {
66 | edits.push(TextEdit.replace(new Range(fs, ln.range.end), 'export ' + ex));
67 | }
68 | }
69 | else { // remove leading and trailing whitespace in quoted string
70 | let fe = tx.indexOf('=');
71 | if (fe > 0) {
72 | let key = tx.substring(0, fe).trim();
73 | let val = tx.substring(fe+1).trim();
74 | if (val.indexOf(' ') >= 0
75 | && (val[0] !== '"' && val[val.length-1] !== '"')
76 | && (val[0] !== '\'' && val[val.length-1] !== '\'')) {
77 | val = `"${val}"`;
78 | }
79 |
80 | edits.push(TextEdit.replace(new Range(fs, ln.range.end), key + '=' + val));
81 | }
82 | }
83 | }
84 | return edits;
85 | }
86 | });
87 |
88 | const foldingRangeProvider = languages.registerFoldingRangeProvider('env', {
89 | provideFoldingRanges(document) {
90 | logger.log(`folding ${document.fileName}`);
91 |
92 | const folds = [];
93 | const start = /^# /, end = /^\s*$/; // regex to detect start and end of region
94 |
95 | let inRegion = false, sectionStart = 0;
96 | for (let i = 0; i < document.lineCount; i++) {
97 | if (start.test(document.lineAt(i).text) && !inRegion) {
98 | inRegion = true;
99 | sectionStart = i;
100 | } else if (end.test(document.lineAt(i).text) && inRegion) {
101 | folds.push(new FoldingRange(sectionStart, i - 1, FoldingRangeKind.Region));
102 | inRegion = false;
103 | }
104 | }
105 | return folds;
106 | }
107 | });
108 |
109 | context.subscriptions.push(logger, formatEditProvider, foldingRangeProvider);
110 | }
111 |
112 | export function deactivate() {}
113 |
--------------------------------------------------------------------------------
/syntaxes/env.tmLanguage.json:
--------------------------------------------------------------------------------
1 | {
2 | "scopeName": "source.env",
3 | "patterns": [
4 | {
5 | "comment": "Comments",
6 | "match": "^\\s?(#.*$)\\n",
7 | "captures": {
8 | "1": {
9 | "patterns": [
10 | {
11 | "include": "#reminder"
12 | }
13 | ]
14 | }
15 | }
16 | },
17 | {
18 | "comment": "Entries",
19 | "match": "^\\s?(export\\s?)*([\\w]+)\\s?(\\=)(.*)$",
20 | "captures": {
21 | "1": {
22 | "name": "keyword.other.env"
23 | },
24 | "2": {
25 | "name": "variable.other.env"
26 | },
27 | "3": {
28 | "name": "keyword.operator.assignment.env"
29 | },
30 | "4": {
31 | "patterns": [
32 | {
33 | "include": "#boolean"
34 | },
35 | {
36 | "include": "#numeric"
37 | },
38 | {
39 | "include": "#string"
40 | },
41 | {
42 | "include": "#interpolated"
43 | },
44 | {
45 | "include": "#unquoted"
46 | }
47 | ]
48 | }
49 | }
50 | }
51 | ],
52 | "repository": {
53 | "reminder": {
54 | "comment": "Reminder - starts with #",
55 | "match": "(#).*",
56 | "name": "comment.line.number-sign.env",
57 | "captures": {
58 | "1": {
59 | "name": "punctuation.definition.comment.env"
60 | }
61 | }
62 | },
63 | "boolean": {
64 | "comment": "Boolean Constants",
65 | "match": "(?i)\\b(true|false|null)\\b(.*)",
66 | "captures": {
67 | "1": {
68 | "name": "constant.language.env"
69 | },
70 | "2": {
71 | "patterns": [
72 | {
73 | "include": "#reminder"
74 | }
75 | ]
76 | }
77 | }
78 | },
79 | "numeric": {
80 | "comment": "Numeric",
81 | "match": "(?:\\+|-)?\\b((?:0(?:x|X)[0-9a-fA-F]*)|(?:(?:[0-9]+\\.?[0-9]*)|(?:\\.[0-9]+))(?:(?:e|E)(?:\\+|-)?[0-9]+)?)\\b(.*)",
82 | "captures": {
83 | "1": {
84 | "name": "constant.numeric.env"
85 | },
86 | "2": {
87 | "patterns": [
88 | {
89 | "include": "#reminder"
90 | }
91 | ]
92 | }
93 | }
94 | },
95 | "string": {
96 | "comment": "Strings (single)",
97 | "name": "string.quoted.single.env",
98 | "begin": "(?