├── .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 | ![GitHub build](https://github.com/IronGeek/vscode-env/workflows/Build/badge.svg) 4 | ![GitHub release (latest by date)](https://img.shields.io/github/v/release/IronGeek/vscode-env?logo=github) 5 | ![GitHub package.json version](https://img.shields.io/github/package-json/v/IronGeek/vscode-env) 6 | ![Visual Studio Marketplace Version](https://img.shields.io/visual-studio-marketplace/v/IronGeek.vscode-env?label=VS%20Marketplace&logo=visual-studio-code) 7 | ![GitHub](https://img.shields.io/github/license/IronGeek/vscode-env) 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 | ![Syntax highlighting](images/highlighting.gif) 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 | ![Folding](images/folding.gif) 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 | ![Formatting](images/formatting.gif) 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 | ![Issue #3: Workaround 1](images/issue-3.1.png) 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 | ![Issue #3: Workaround 2](images/issue-3.2.png) 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": "(?