├── .gitignore ├── resources ├── demo.gif ├── icon.png ├── demo_smart.gif └── icon.svg ├── .vscodeignore ├── .vscode ├── extensions.json ├── tasks.json └── launch.json ├── .eslintrc.json ├── src ├── test │ ├── suite │ │ ├── extension.test.ts │ │ └── index.ts │ └── runTest.ts ├── providers │ ├── CensoringCodeLensProvider.ts │ ├── CensoringProvider.ts │ └── ConfigurationProvider.ts ├── decorations │ └── CensorBar.ts └── extension.ts ├── tsconfig.json ├── LICENSE.md ├── .github └── workflows │ └── release.yml ├── webpack.config.js ├── vsc-extension-quickstart.md ├── README.md ├── syntaxes └── censitive.tmLanguage.json ├── package.json └── CHANGELOG.md /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | dist 3 | node_modules 4 | .vscode-test/ 5 | *.vsix 6 | .vscode/settings.json 7 | -------------------------------------------------------------------------------- /resources/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1nVitr0/plugin-vscode-censitive/HEAD/resources/demo.gif -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1nVitr0/plugin-vscode-censitive/HEAD/resources/icon.png -------------------------------------------------------------------------------- /resources/demo_smart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/1nVitr0/plugin-vscode-censitive/HEAD/resources/demo_smart.gif -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/** 4 | src/** 5 | .gitignore 6 | .yarnrc 7 | vsc-extension-quickstart.md 8 | **/tsconfig.json 9 | **/.eslintrc.json 10 | **/*.map 11 | **/*.ts 12 | -------------------------------------------------------------------------------- /.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 | "eamodio.tsl-problem-matcher" 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/test/suite/extension.test.ts: -------------------------------------------------------------------------------- 1 | import * as assert from 'assert'; 2 | 3 | // You can import and use all API from the 'vscode' module 4 | // as well as import your extension to test it 5 | import * as vscode from 'vscode'; 6 | // import * as myExtension from '../../extension'; 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.strictEqual(-1, [1, 2, 3].indexOf(5)); 13 | assert.strictEqual(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /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 /* enable all strict type-checking options */ 12 | /* Additional Checks */ 13 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 14 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 15 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 16 | }, 17 | "exclude": [ 18 | "node_modules", 19 | ".vscode-test" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | 3 | import { runTests } from "vscode-test"; 4 | 5 | async function main() { 6 | try { 7 | // The folder containing the Extension Manifest package.json 8 | // Passed to `--extensionDevelopmentPath` 9 | const extensionDevelopmentPath = path.resolve(__dirname, "../../"); 10 | 11 | // The path to test runner 12 | // Passed to --extensionTestsPath 13 | const extensionTestsPath = path.resolve(__dirname, "./suite/index"); 14 | 15 | // Download VS Code, unzip it and run the integration test 16 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 17 | } catch (err) { 18 | console.error("Failed to run tests"); 19 | process.exit(1); 20 | } 21 | } 22 | 23 | main(); 24 | -------------------------------------------------------------------------------- /.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": "compile", 9 | "problemMatcher": ["$ts-webpack-watch", "$tslint-webpack-watch"], 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | }, 19 | { 20 | "type": "npm", 21 | "script": "test-watch", 22 | "problemMatcher": "$tsc-watch", 23 | "isBackground": true, 24 | "presentation": { 25 | "reveal": "never" 26 | }, 27 | "group": "build" 28 | } 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /src/test/suite/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import * as Mocha from "mocha"; 3 | import { glob } from "glob"; 4 | 5 | export function run(): Promise { 6 | // Create the mocha test 7 | const mocha = new Mocha({ 8 | ui: "tdd", 9 | color: true, 10 | }); 11 | 12 | const testsRoot = path.resolve(__dirname, ".."); 13 | 14 | return new Promise(async (c, e) => { 15 | const files = await glob("**/**.test.js", { cwd: testsRoot }); 16 | 17 | // Add files to the test suite 18 | files.forEach((f) => mocha.addFile(path.resolve(testsRoot, f))); 19 | 20 | try { 21 | // Run the mocha test 22 | mocha.run((failures) => { 23 | if (failures > 0) { 24 | e(new Error(`${failures} tests failed.`)); 25 | } else { 26 | c(); 27 | } 28 | }); 29 | } catch (err) { 30 | console.error(err); 31 | e(err); 32 | } 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /.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}/dist/**/*.js" 17 | ], 18 | "preLaunchTask": "${defaultBuildTask}" 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "npm: test-watch" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Aram Becker 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. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - "main" 7 | 8 | jobs: 9 | bump_version: 10 | name: Bump version 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | with: 16 | fetch-depth: 0 17 | 18 | - name: Setup Node.js 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: 20.x 22 | 23 | - name: Install dependencies 24 | run: npm ci 25 | 26 | - name: Publish release 27 | env: 28 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 29 | VSCE_PAT: ${{ secrets.VSCE_PAT }} 30 | OVSX_PAT: ${{ secrets.OVSX_PAT }} 31 | run: npx semantic-release 32 | 33 | update_develop: 34 | name: Update develop Branch 35 | runs-on: ubuntu-latest 36 | needs: bump_version 37 | steps: 38 | - name: Checkout develop Branch 39 | uses: actions/checkout@v4 40 | with: 41 | fetch-depth: 0 42 | ref: develop 43 | - name: Git Config 44 | run: | 45 | git config --local user.email 'action@github.com' 46 | git config --local user.name 'GitHub Action' 47 | - name: Merge main Branch into develop (Rebase) 48 | run: git rebase origin/main 49 | - name: Push develop Branch 50 | run: git push 51 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | /**@type {import('webpack').Configuration}*/ 8 | const config = { 9 | target: 'node', // vscode extensions run in a Node.js-context 📖 -> https://webpack.js.org/configuration/node/ 10 | mode: 'none', // this leaves the source code as close as possible to the original (when packaging we set this to 'production') 11 | 12 | entry: './src/extension.ts', // the entry point of this extension, 📖 -> https://webpack.js.org/configuration/entry-context/ 13 | output: { 14 | // the bundle is stored in the 'dist' folder (check package.json), 📖 -> https://webpack.js.org/configuration/output/ 15 | path: path.resolve(__dirname, 'dist'), 16 | filename: 'extension.js', 17 | libraryTarget: 'commonjs2' 18 | }, 19 | devtool: 'nosources-source-map', 20 | externals: { 21 | vscode: 'commonjs vscode' // the vscode-module is created on-the-fly and must be excluded. Add other modules that cannot be webpack'ed, 📖 -> https://webpack.js.org/configuration/externals/ 22 | }, 23 | resolve: { 24 | // support reading TypeScript and JavaScript files, 📖 -> https://github.com/TypeStrong/ts-loader 25 | extensions: ['.ts', '.js'] 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: 'ts-loader' 35 | } 36 | ] 37 | } 38 | ] 39 | } 40 | }; 41 | module.exports = config; -------------------------------------------------------------------------------- /src/providers/CensoringCodeLensProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CancellationToken, 3 | CodeLens, 4 | CodeLensProvider, 5 | Disposable, 6 | Event, 7 | EventEmitter, 8 | ProviderResult, 9 | Range, 10 | TextDocument, 11 | } from "vscode"; 12 | 13 | export default class CensoringCodeLensProvider implements CodeLensProvider { 14 | private onDidChange: EventEmitter; 15 | private censorMap: { [fileName: string]: { censored: Range[]; visible: Range[] } } = {}; 16 | 17 | public constructor() { 18 | this.onDidChange = new EventEmitter(); 19 | } 20 | 21 | get onDidChangeCodeLenses(): Event { 22 | return this.onDidChange.event; 23 | } 24 | 25 | public setCensoredRanges({ fileName }: TextDocument, censored: Range[], visible: Range[]): Disposable { 26 | this.censorMap[fileName] = { censored, visible }; 27 | this.onDidChange.fire(); 28 | 29 | return { 30 | dispose: () => delete this.censorMap[fileName], 31 | }; 32 | } 33 | 34 | public provideCodeLenses({ fileName }: TextDocument, token: CancellationToken): ProviderResult { 35 | const { censored, visible } = this.censorMap[fileName] || { censored: [], visible: [] }; 36 | let lineIndex: number[] = []; 37 | 38 | const lenses: CodeLens[] = censored.map((range) => { 39 | const index = (lineIndex[range.start.line] = (lineIndex[range.start.line] || 0) + 1); 40 | return new CodeLens(range, { 41 | title: index > 1 ? `$(copy) Copy #${index} to Clipboard` : "$(copy) Copy to Clipboard", 42 | command: "censitive.copyCensoredRange", 43 | arguments: [range], 44 | }); 45 | }); 46 | 47 | lineIndex = []; 48 | for (const range of censored.filter((range) => !visible.some((v) => v.isEqual(range)))) { 49 | const index = (lineIndex[range.start.line] = (lineIndex[range.start.line] || 0) + 1); 50 | lenses.push( 51 | new CodeLens(range, { 52 | title: index > 1 ? `$(unlock) Show Censored Text #${index}` : "$(unlock) Show Censored Text", 53 | command: "censitive.displayCensoredRange", 54 | arguments: [range], 55 | }) 56 | ); 57 | } 58 | 59 | return lenses; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/decorations/CensorBar.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DecorationRangeBehavior, 3 | DecorationRenderOptions, 4 | TextEditorDecorationType, 5 | ThemableDecorationAttachmentRenderOptions, 6 | ThemeColor, 7 | window, 8 | } from "vscode"; 9 | 10 | export interface CensorOptions { 11 | border?: string; 12 | grow?: boolean; 13 | postfix?: string; 14 | prefix?: string; 15 | color?: string; 16 | } 17 | 18 | export default class CensorBar { 19 | private _decoration?: TextEditorDecorationType; 20 | private _options: CensorOptions; 21 | 22 | public constructor(options: CensorOptions) { 23 | this._options = options; 24 | } 25 | 26 | public get decoration(): TextEditorDecorationType { 27 | if (!this._decoration) { 28 | return this.generateDecoration(); 29 | } else { 30 | return this._decoration; 31 | } 32 | } 33 | 34 | private static buildRenderAttachment(contentText?: string): ThemableDecorationAttachmentRenderOptions | undefined { 35 | if (!contentText) { 36 | return undefined; 37 | } 38 | 39 | return { contentText }; 40 | } 41 | 42 | public setCensorType(options: CensorOptions) { 43 | this._options = options; 44 | } 45 | 46 | public regenerateDecoration() { 47 | const dispose = this._decoration && this._decoration.dispose; 48 | this._decoration = undefined; 49 | this.generateDecoration(); 50 | return dispose; 51 | } 52 | 53 | private generateDecoration(): TextEditorDecorationType { 54 | this._decoration = window.createTextEditorDecorationType({ 55 | before: CensorBar.buildRenderAttachment(this._options.prefix), 56 | after: CensorBar.buildRenderAttachment(this._options.postfix), 57 | rangeBehavior: this._options.grow ? DecorationRangeBehavior.OpenOpen : DecorationRangeBehavior.ClosedClosed, 58 | ...this.getDecorationParams(), 59 | }); 60 | 61 | return this._decoration; 62 | } 63 | 64 | private getDecorationParams(): DecorationRenderOptions { 65 | const backgroundColor = 66 | this._options.color && /^theme./.test(this._options.color) 67 | ? new ThemeColor(this._options.color.replace(/^theme./, "")) 68 | : this._options.color; 69 | 70 | return { border: this._options.border, backgroundColor, opacity: "0" }; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your VS Code Extension 2 | 3 | ## What's in the folder 4 | 5 | * This folder contains all of the files necessary for your extension. 6 | * `package.json` - this is the manifest file in which you declare your extension and command. 7 | * The sample plugin registers a command and defines its title and command name. With this information VS Code can show the command in the command palette. It doesn’t yet need to load the plugin. 8 | * `src/extension.ts` - this is the main file where you will provide the implementation of your command. 9 | * The file exports one function, `activate`, which is called the very first time your extension is activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 10 | * We pass the function containing the implementation of the command as the second parameter to `registerCommand`. 11 | 12 | ## Get up and running straight away 13 | 14 | * Press `F5` to open a new window with your extension loaded. 15 | * Run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World`. 16 | * Set breakpoints in your code inside `src/extension.ts` to debug your extension. 17 | * Find output from your extension in the debug console. 18 | 19 | ## Make changes 20 | 21 | * You can relaunch the extension from the debug toolbar after changing code in `src/extension.ts`. 22 | * You can also reload (`Ctrl+R` or `Cmd+R` on Mac) the VS Code window with your extension to load your changes. 23 | 24 | 25 | ## Explore the API 26 | 27 | * You can open the full set of our API when you open the file `node_modules/@types/vscode/index.d.ts`. 28 | 29 | ## Run tests 30 | 31 | * Open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Extension Tests`. 32 | * Press `F5` to run the tests in a new window with your extension loaded. 33 | * See the output of the test result in the debug console. 34 | * Make changes to `src/test/suite/extension.test.ts` or create new test files inside the `test/suite` folder. 35 | * The provided test runner will only consider files matching the name pattern `**.test.ts`. 36 | * You can create folders inside the `test` folder to structure your tests any way you want. 37 | 38 | ## Go further 39 | 40 | * Reduce the extension size and improve the startup time by [bundling your extension](https://code.visualstudio.com/api/working-with-extensions/bundling-extension). 41 | * [Publish your extension](https://code.visualstudio.com/api/working-with-extensions/publishing-extension) on the VSCode extension marketplace. 42 | * Automate builds by setting up [Continuous Integration](https://code.visualstudio.com/api/working-with-extensions/continuous-integration). 43 | -------------------------------------------------------------------------------- /resources/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 12 | 14 | 16 | 21 | 30 | 39 | 48 | 57 | 61 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Visual Studio Code extension 1nVitr0.censitive](https://img.shields.io/visual-studio-marketplace/v/1nVitr0.censitive?logo=visualstudiocode)](https://marketplace.visualstudio.com/items?itemName=1nVitr0.censitive) 2 | [![Open VSX extension 1nVitr0.censitive](https://img.shields.io/open-vsx/v/1nVitr0/censitive)](https://open-vsx.org/extension/1nVitr0/censitive) 3 | [![Installs for Visual Studio Code extension 1nVitr0.censitive](https://img.shields.io/visual-studio-marketplace/i/1nVitr0.censitive?logo=visualstudiocode)](https://marketplace.visualstudio.com/items?itemName=1nVitr0.censitive) 4 | [![Rating for Visual Studio Code extension 1nVitr0.censitive](https://img.shields.io/visual-studio-marketplace/r/1nVitr0.censitive?logo=visualstudiocode)](https://marketplace.visualstudio.com/items?itemName=1nVitr0.censitive) 5 | [![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release) 6 | 7 | # Censitive (Hide Passwords and Tokens) 8 | 9 | Censitive censors all your sensitive information, such as database logins, tokens or keys. 10 | 11 | ![demo for .env files](https://raw.githubusercontent.com/1nVitr0/plugin-vscode-censitive/main/resources/demo.gif) 12 | 13 | *Please be aware that this extension does __NOT__ guarantee that your private information stays hidden!* 14 | *There is an __unavoidable delay__ between opening a document and the data being censored.* 15 | 16 | ### To get started 17 | 18 | 1. Create a `.censitive` file in the root of your workspace (or in your home folder), e.g.: 19 | > ```censitive 20 | > // Format :[keyRegex] 21 | > .env:.*_KEY,.*_token,.*_PassWord 22 | > id_rsa:* 23 | > ``` 24 | 3. Open a file of the type specified in `.censitive` and check the censoring 25 | 26 | ## Features 27 | 28 | The extension uses decorations to block out sensitive information as set in a `.censitive` file in a parent directory or in your home directory. 29 | If both files are present and `censtitive.mergeGlobalCensoring` is enabled, their censoring will be merged. 30 | 31 | When active, the extension will censor all content set in `.censitive` file(s) by using a key-value approach. 32 | This means, the `.censitive` file specifies key regexes and the extension automatically finds values assigned to these keys. 33 | However, because the censoring is based solely on regex, some value formats may not be recognized. 34 | 35 | ![demo for js files](https://raw.githubusercontent.com/1nVitr0/plugin-vscode-censitive/main/resources/demo_smart.gif) 36 | 37 | Two code actions "Copy to Clipboard" and "Show Censored Text" are provided for convenient access to the censored text. 38 | 39 | ## Extension Settings 40 | 41 | This extension has the following settings: 42 | 43 | * `censtitive.enable`: enable/disable this extension 44 | * `censitive.codeLanguages`: List of code languages that conform to standard code syntax (e.g. `c`, `cpp`, `javascript`, `python`) 45 | * `censitive.assignmentRegex`: Regex used to detect assignments, usually begin and end with `[\\t ]` to capture surrounding spaces 46 | * `censtitive.mergeGlobalCensoring`: merge configuration in your home directory with the workspace settings 47 | * `censitive.useFastModeMinLines`: above this line threshold the document is censored twice: once for the visible range and once for the entire document. This speeds up censoring marginally, but can still be slow 48 | * `censtitive.censor`: Visual settings used for censoring 49 | * `censtitive.showTimeoutSeconds`: Controls the time the password is shown after clicking on 'Show Censored Text' 50 | * `censitive.defaultCensoring`: Default censoring config, if no `.censitive` file is present in the workspace or the user's home directory 51 | * `censitive.mergeDefaultCensoring`: merge default configuration with all .censitive configurations 52 | 53 | The values being censored can be controlled using a `.censitive` file in the workspace root, or in any other directory. 54 | The keys are matched case insensitive. Its basic format is: 55 | 56 | ```censitive 57 | # Comment 58 | :[keyRegex] 59 | !:[keyRegex] 60 | 61 | # Alternatively, for fenced censoring 62 | !:[beginRegex]:[endRegex] 63 | 64 | # Or, for more complex censoring 65 | !:[beginRegex],[keyRegex]:[endRegex] 66 | ``` 67 | 68 | The glob pattern is always taken relative to the directory the .censitive file is located in. 69 | If there is no active workspace, all patterns are automatically prepended with `**/`. 70 | 71 | Multiple key regular expressions can be provided, by separating them with a comma `,`. 72 | When providing fenced censoring, the amount of comma-separated end expressions must match the amount of start expressions. 73 | Additional start expressions will be used as keys, additional end expressions will be ignored. 74 | 75 | Exclude patterns can be added after the ``, separated by a `!`. 76 | They behave the same way as the `` and can be used to exclude specific files from censoring. 77 | They only correspond to the preceding glob pattern and do not exclude files from other `.censitive` lines. 78 | 79 | For example: 80 | 81 | ```censitive 82 | # Hide the following variables in .env files 83 | .env:.*_KEY,.*_token,.*_PassWord 84 | 85 | # Hide all passwords and api tokens in js and ts files 86 | *.{js,ts}:apitoken,.*password 87 | 88 | # Hide Certificates and keys 89 | *.{pem,crt,key}:BEGIN CERTIFICATE,BEGIN (RSA )?PRIVATE KEY:END CERTIFICATE,END (RSA )?PRIVATE KEY 90 | 91 | # Hide passwords in env files, but not in the env.example file 92 | env*!env.example:.*password 93 | ``` 94 | 95 | To completely hide the content of specific files, the shorthand `*` can be used as the key: 96 | 97 | ```censitive 98 | # Hide the content of private keys 99 | **/id_rsa:* 100 | ``` 101 | 102 | ## Known Issues 103 | 104 | * For large documents, it will take some time for the values to get censored. This is unavoidable due to VS Code processing the document before any extensions. 105 | * The censoring may flicker when changing between opened files in the editor 106 | * At the moment, there is no option to add custom regular expressions. 107 | * `.*` will be automatically transformed to `[^\s]*` to enable multiple censors in a single line. This means: keys with spaces might behave differently than expected. 108 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import { 2 | TextEditor, 3 | ExtensionContext, 4 | workspace, 5 | window, 6 | commands, 7 | TextDocument, 8 | FileSystemWatcher, 9 | RelativePattern, 10 | Uri, 11 | WorkspaceFolder, 12 | extensions, 13 | Range, 14 | env, 15 | languages, 16 | Disposable, 17 | } from "vscode"; 18 | import CensoringCodeLensProvider from "./providers/CensoringCodeLensProvider"; 19 | import CensoringProvider from "./providers/CensoringProvider"; 20 | import ConfigurationProvider, { Configuration } from "./providers/ConfigurationProvider"; 21 | import { dirname, join } from "path"; 22 | 23 | let configurationProvider = new ConfigurationProvider(); 24 | let censoringCodeLensProvider: CensoringCodeLensProvider; 25 | let instanceMap = new Map(); 26 | 27 | export async function activate(context: ExtensionContext) { 28 | await ConfigurationProvider.init(); 29 | const { config, userHome } = configurationProvider; 30 | 31 | context.subscriptions.push( 32 | languages.registerCodeLensProvider( 33 | { pattern: "**/*" }, 34 | (censoringCodeLensProvider = new CensoringCodeLensProvider()) 35 | ), 36 | commands.registerCommand("censitive.toggleCensoring", () => { 37 | const enabled = configurationProvider.toggleEnable(); 38 | window.showInformationMessage(`Censoring ${enabled ? "enabled" : "disabled"}.`); 39 | }), 40 | commands.registerTextEditorCommand("censitive.copyCensoredRange", (editor, _, range?: Range) => { 41 | if (!range) { 42 | return window.showErrorMessage("No censored field found. This command should not be triggered manually."); 43 | } 44 | 45 | const text = editor.document.getText(range); 46 | env.clipboard.writeText(text); 47 | window.showInformationMessage("Censored field copied to clipboard!"); 48 | }), 49 | commands.registerTextEditorCommand("censitive.displayCensoredRange", async (editor, _, range?: Range) => { 50 | if (!range) { 51 | return window.showErrorMessage("No censored field found. This command should not be triggered manually."); 52 | } 53 | 54 | const censoring = await findOrCreateInstance(editor.document); 55 | 56 | censoring.addVisibleRange(range); 57 | censoring.applyCensoredRanges(); 58 | setTimeout(() => { 59 | censoring.removeVisibleRange(range); 60 | censoring.applyCensoredRanges(); 61 | }, config.showTimeoutSeconds * 1000); 62 | }), 63 | createConfigWatcher() 64 | ); 65 | 66 | window.onDidChangeVisibleTextEditors(onVisibleEditorsChanged, null, context.subscriptions); 67 | workspace.onDidCloseTextDocument(onCloseDocument, null, context.subscriptions); 68 | workspace.onDidChangeConfiguration(onConfigurationChange, null, context.subscriptions); 69 | extensions.onDidChange(onConfigurationChange, null, context.subscriptions); 70 | 71 | if (userHome) { 72 | context.subscriptions.push(createConfigWatcher(userHome)); 73 | await onCensorConfigChanged(Uri.file(join(userHome, ".censitive"))); 74 | } 75 | 76 | await onVisibleEditorsChanged(window.visibleTextEditors); 77 | } 78 | 79 | export function deactivate() { 80 | instanceMap.forEach((instance) => instance.dispose()); 81 | instanceMap.clear(); 82 | } 83 | 84 | function createConfigWatcher(folder?: Uri | WorkspaceFolder | string) { 85 | const pattern = 86 | typeof folder === "string" 87 | ? new RelativePattern(Uri.file(join(folder, ".censitive")), "*") 88 | : folder 89 | ? new RelativePattern(folder, ".censitive") 90 | : "**/.censitive"; 91 | const configWatcher = workspace.createFileSystemWatcher(pattern); 92 | configWatcher.onDidCreate(onCensorConfigChanged); 93 | configWatcher.onDidChange(onCensorConfigChanged); 94 | configWatcher.onDidDelete(onCensorConfigChanged); 95 | 96 | return configWatcher; 97 | } 98 | 99 | async function isValidDocument(config: Configuration, document: TextDocument): Promise { 100 | return config.enable && (await configurationProvider.isDocumentInCensorConfig(document)); 101 | } 102 | 103 | async function findOrCreateInstance(document: TextDocument) { 104 | const found = instanceMap.get(document.uri.toString()); 105 | 106 | if (!found) { 107 | const instance = new CensoringProvider(document, configurationProvider.censorOptions, censoringCodeLensProvider); 108 | instanceMap.set(document.uri.toString(), instance); 109 | } 110 | 111 | return found || instanceMap.get(document.uri.toString())!; 112 | } 113 | 114 | async function doCensoring(documents: TextDocument[] = [], configChanged = false) { 115 | if (documents.length) { 116 | await Promise.all( 117 | documents.map(async (document) => { 118 | const instance = await findOrCreateInstance(document); 119 | instance.censor(true, configChanged); 120 | }) 121 | ); 122 | } 123 | } 124 | 125 | async function onConfigurationChange() { 126 | await ConfigurationProvider.updateConfig(); 127 | onVisibleEditorsChanged(window.visibleTextEditors); 128 | } 129 | 130 | async function onCensorConfigChanged(uri: Uri) { 131 | const { userHome } = configurationProvider; 132 | const workspaceFolder = workspace.getWorkspaceFolder(uri) ?? null; 133 | const parentFolder = Uri.file(dirname(uri.fsPath)); 134 | 135 | await ConfigurationProvider.updateCensoringKeys( 136 | workspaceFolder, 137 | uri, 138 | parentFolder.toString() === workspaceFolder?.uri.toString() 139 | ? workspaceFolder 140 | : parentFolder.fsPath === userHome 141 | ? null 142 | : parentFolder 143 | ); 144 | await onVisibleEditorsChanged(window.visibleTextEditors, true); 145 | } 146 | 147 | function onCloseDocument(document: TextDocument) { 148 | // Dispose instance if document is closed 149 | instanceMap.delete(document.uri.toString()); 150 | } 151 | 152 | async function onVisibleEditorsChanged(visibleEditors: readonly TextEditor[], configChanged = false) { 153 | const { config } = configurationProvider; 154 | const visibleDocuments = visibleEditors.map(({ document }) => document); 155 | 156 | // Only update visible TextEditors with valid configuration 157 | const validDocuments = ( 158 | await Promise.all( 159 | visibleDocuments.map(async (document) => ((await isValidDocument(config, document)) ? document : false)) 160 | ) 161 | ).filter(Boolean) as TextDocument[]; 162 | await doCensoring(validDocuments, configChanged); 163 | 164 | if (configChanged) { 165 | for (const [document, instance] of instanceMap) { 166 | if (!(await isValidDocument(config, instance.document))) { 167 | instance.dispose(); 168 | instanceMap.delete(document); 169 | } 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /syntaxes/censitive.tmLanguage.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/martinring/tmlanguage/master/tmlanguage.json", 3 | "name": "censitive", 4 | "scopeName": "source.censitive", 5 | "fileTypes": [ 6 | ".censitive" 7 | ], 8 | "patterns": [ 9 | { 10 | "match": "^(#|\\/\\/).*$", 11 | "comment": "Comment line", 12 | "name": "comment.line.censitive", 13 | "captures": { 14 | "1": { 15 | "name": "punctuation.definition.comment.censitive" 16 | } 17 | } 18 | }, 19 | { 20 | "comment": "Glob Exclude Pattern", 21 | "match": "^!([^:]+)$", 22 | "name": "meta.block.censitive meta.entry.censitive", 23 | "captures": { 24 | "1": { 25 | "name": "support.type.file-selector.censitive string.glob.censitive", 26 | "patterns": [ 27 | { 28 | "include": "#glob" 29 | } 30 | ] 31 | } 32 | } 33 | }, 34 | { 35 | "comment": "Glob Pattern", 36 | "match": "^(.+?)((!)(.+?))?((?]?[\\t ]*", 87 | "yaml": "[\\t ]*:[\\t ]*(?!>|\\|)", 88 | "cpp": "(?:[\\t ]*=[\\t ]*|(?<=^#define[\\t ]+.+)[\\t ]+)", 89 | "c": "(?:[\\t ]*=[\\t ]*|(?<=^#define[\\t ]+.+)[\\t ]+)" 90 | }, 91 | "description": "Regex used to detect assignments", 92 | "type": "object", 93 | "items": { 94 | "type": "string" 95 | }, 96 | "required": [ 97 | "default" 98 | ] 99 | }, 100 | "censitive.showTimeoutSeconds": { 101 | "default": 10, 102 | "description": "Time a censored text is shown after clicking 'Show Censored Text'", 103 | "type": "integer" 104 | }, 105 | "censitive.useFastModeMinLines": { 106 | "default": 10000, 107 | "description": "The lines above which censoring is performed for the visible range first to speed up censoring.", 108 | "type": "integer" 109 | }, 110 | "censitive.mergeGlobalCensoring": { 111 | "default": true, 112 | "description": "Merge global and local .censitive files", 113 | "type": "boolean" 114 | }, 115 | "censitive.mergeDefaultCensoring": { 116 | "default": true, 117 | "description": "Merge default config with all .censitive files", 118 | "type": "boolean" 119 | }, 120 | "censitive.defaultCensoring": { 121 | "default": [ 122 | { 123 | "match": "**/{env,.env,env.*,.env.*}", 124 | "exclude": "**/{env.example,.env.example}", 125 | "censor": [ 126 | ".*password", 127 | ".*token", 128 | ".*secret.*" 129 | ] 130 | }, 131 | { 132 | "match": "**/id_{rsa,dsa,ecdsa,eddsa,dss,sha2}", 133 | "censor": [ 134 | { 135 | "start": "-----BEGIN.*PRIVATE KEY-----", 136 | "end": "-----END.*PRIVATE KEY-----" 137 | } 138 | ] 139 | } 140 | ], 141 | "description": "Default censoring rules for all files", 142 | "type": "array", 143 | "items": { 144 | "type": "object", 145 | "properties": { 146 | "match": { 147 | "default": "**/{env,env.*}", 148 | "description": "glob pattern to match files", 149 | "type": "string", 150 | "minLength": 1 151 | }, 152 | "exclude": { 153 | "description": "glob pattern to exclude files", 154 | "type": "string", 155 | "minLength": 1 156 | }, 157 | "censor": { 158 | "type": "array", 159 | "minItems": 1, 160 | "items": { 161 | "oneOf": [ 162 | { 163 | "default": ".*password", 164 | "type": "string", 165 | "minLength": 1 166 | }, 167 | { 168 | "type": "object", 169 | "properties": { 170 | "start": { 171 | "default": "---BEGIN SECRET KEY---", 172 | "description": "start of the sensitive content", 173 | "type": "string", 174 | "minLength": 1 175 | }, 176 | "end": { 177 | "default": "---END SECRET KEY---", 178 | "description": "end of the sensitive content", 179 | "type": "string", 180 | "minLength": 1 181 | } 182 | } 183 | } 184 | ] 185 | } 186 | } 187 | }, 188 | "required": [ 189 | "match", 190 | "censor" 191 | ] 192 | } 193 | }, 194 | "censitive.censoring": { 195 | "type": "object", 196 | "default": { 197 | "color": "theme.editorInfo.background", 198 | "prefix": "🔒", 199 | "border": "2px solid grey", 200 | "grow": true 201 | }, 202 | "description": "Visual settings used for censoring", 203 | "properties": { 204 | "prefix": { 205 | "type": "string", 206 | "default": "🔒", 207 | "description": "text displayed before the censored content" 208 | }, 209 | "postfix": { 210 | "type": "string", 211 | "description": "text displayed after the censored content" 212 | }, 213 | "grow": { 214 | "type": "boolean", 215 | "default": true, 216 | "description": "grow censoring when the text is edited (when disabled single letters might be visible before censored)" 217 | }, 218 | "border": { 219 | "type": "string", 220 | "description": "css border around censored content" 221 | }, 222 | "color": { 223 | "type": "string", 224 | "default": "theme.editorInfo.background", 225 | "description": "color of the censor bar. use `theme.` prefix to use theme colors (e.g. `theme.foreground`)" 226 | } 227 | } 228 | } 229 | } 230 | }, 231 | "grammars": [ 232 | { 233 | "language": "censitive", 234 | "scopeName": "source.censitive", 235 | "path": "./syntaxes/censitive.tmLanguage.json" 236 | } 237 | ], 238 | "languages": [ 239 | { 240 | "id": "censitive", 241 | "filenames": [ 242 | ".censitive" 243 | ] 244 | } 245 | ] 246 | }, 247 | "scripts": { 248 | "vscode:prepublish": "npm run package", 249 | "compile": "webpack", 250 | "watch": "webpack --watch", 251 | "package": "webpack --mode production --devtool hidden-source-map", 252 | "test-compile": "tsc -p ./", 253 | "test-watch": "tsc -watch -p ./", 254 | "pretest": "npm run test-compile && npm run lint", 255 | "lint": "eslint src --ext ts", 256 | "test": "node ./out/test/runTest.js" 257 | }, 258 | "devDependencies": { 259 | "@semantic-release/changelog": "^6.0.3", 260 | "@semantic-release/git": "^10.0.1", 261 | "@types/glob": "^8.1.0", 262 | "@types/mocha": "^10.0.6", 263 | "@types/node": "^18.15.5", 264 | "@types/vscode": "^1.53.0", 265 | "@typescript-eslint/eslint-plugin": "^7.13.0", 266 | "@typescript-eslint/parser": "^7.13.0", 267 | "eslint": "^8.56.0", 268 | "glob": "^10.4.1", 269 | "mocha": "^10.4.0", 270 | "semantic-release": "^24.0.0", 271 | "semantic-release-vsce": "^5.7.1", 272 | "ts-loader": "^9.5.1", 273 | "typescript": "^5.4.5", 274 | "vscode-test": "^1.5.0", 275 | "webpack": "^5.94.0", 276 | "webpack-cli": "^5.1.4" 277 | }, 278 | "release": { 279 | "branches": [ 280 | "main", 281 | "develop" 282 | ], 283 | "plugins": [ 284 | "@semantic-release/commit-analyzer", 285 | "@semantic-release/release-notes-generator", 286 | "@semantic-release/changelog", 287 | [ 288 | "@semantic-release/git", 289 | { 290 | "message": "chore(release): ${nextRelease.version}\n\n${nextRelease.notes}", 291 | "assets": [ 292 | "package.json", 293 | "CHANGELOG.md", 294 | "README.md" 295 | ] 296 | } 297 | ], 298 | [ 299 | "semantic-release-vsce", 300 | { 301 | "packageVsix": true 302 | } 303 | ], 304 | "@semantic-release/github" 305 | ] 306 | } 307 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.5.2](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.5.1...v1.5.2) (2024-11-03) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * support censoring for `#define` in cpp / c ([143776d](https://github.com/1nVitr0/plugin-vscode-censitive/commit/143776d5d92265f088f8ffd910050d3c2494d36b)) 7 | 8 | ## [1.5.1](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.5.0...v1.5.1) (2024-10-28) 9 | 10 | 11 | ### Bug Fixes 12 | 13 | * always use default censoring if enabled ([ddbe0dd](https://github.com/1nVitr0/plugin-vscode-censitive/commit/ddbe0dd8e05cfe3afa50c13f8a747c3b81ed3879)) 14 | * init and watch only config file in home directory ([8b3d2e8](https://github.com/1nVitr0/plugin-vscode-censitive/commit/8b3d2e8920038a1e7c16dc3591527c2239b895f1)) 15 | 16 | # [1.5.0](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.4.1...v1.5.0) (2024-10-11) 17 | 18 | 19 | ### Bug Fixes 20 | 21 | * always update censoring config on change across workspaces ([3a3649d](https://github.com/1nVitr0/plugin-vscode-censitive/commit/3a3649dd9b2e26bca15cda7ba31532cd9d526dde)) 22 | * make changing config more efficient ([0060451](https://github.com/1nVitr0/plugin-vscode-censitive/commit/0060451fdac55f46c98ca970958a4a706ebf7368)) 23 | 24 | 25 | ### Features 26 | 27 | * support subdirectories and .censitive files outside workspace ([2bdbe93](https://github.com/1nVitr0/plugin-vscode-censitive/commit/2bdbe93ef7cb3a1247f13522072264cbfeb591d0)) 28 | * support subdirectories inside workspace ([c32ab0d](https://github.com/1nVitr0/plugin-vscode-censitive/commit/c32ab0d050b9faf81284282214d8f8d9910c592b)) 29 | 30 | ## [1.4.1](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.4.0...v1.4.1) (2024-09-09) 31 | 32 | 33 | ### Bug Fixes 34 | 35 | * skip recalculating ranges when document is unchanged ([2707bc1](https://github.com/1nVitr0/plugin-vscode-censitive/commit/2707bc12c6b68dbac6c5afbe9ac80747b18febc6)) 36 | 37 | # [1.4.0](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.3.1...v1.4.0) (2024-06-14) 38 | 39 | 40 | ### Features 41 | 42 | * add config for default censoring ([1e0b650](https://github.com/1nVitr0/plugin-vscode-censitive/commit/1e0b650bdbfadeeebf8f2470880694ea92a59666)) 43 | * add option to exclude globs from censoring ([c58c189](https://github.com/1nVitr0/plugin-vscode-censitive/commit/c58c189f8025b37f6777c406c23d3770432ccf93)) 44 | 45 | ## [1.3.1](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.3.0...v1.3.1) (2023-06-21) 46 | 47 | 48 | ### Bug Fixes 49 | 50 | * prevent censoring every line ([f64e022](https://github.com/1nVitr0/plugin-vscode-censitive/commit/f64e0229404af47d86b4a19e58b63652862eae9c)) 51 | 52 | # [1.3.0](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.2.7...v1.3.0) (2023-06-12) 53 | 54 | 55 | ### Features 56 | 57 | * make code languages & assignment configurable ([782cc88](https://github.com/1nVitr0/plugin-vscode-censitive/commit/782cc88781f7be4e01044f5f73ed376283eaa407)) 58 | * support fenced censored blocks ([8922078](https://github.com/1nVitr0/plugin-vscode-censitive/commit/8922078362cfe7f935acd39fee07647e6017d005)) 59 | 60 | ## [1.2.7](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.2.6...v1.2.7) (2023-03-25) 61 | 62 | 63 | ### Bug Fixes 64 | 65 | * document `**/` glob patterns ([4848888](https://github.com/1nVitr0/plugin-vscode-censitive/commit/48488883e8fa1a2e9827824647cb070f132410e8)) 66 | 67 | ## [1.2.6](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.2.5...v1.2.6) (2023-03-25) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * censor visible editors on launch ([d05fc51](https://github.com/1nVitr0/plugin-vscode-censitive/commit/d05fc518b1b6acac2b8a04fc52688b8da5e7ad47)) 73 | * dispose censoring when file is removed from config ([311737a](https://github.com/1nVitr0/plugin-vscode-censitive/commit/311737a618d6b45bf0c8e86918f40892c0de44c8)) 74 | * global censitive configuration was permanently merged into workspaces ([a091aea](https://github.com/1nVitr0/plugin-vscode-censitive/commit/a091aea4e8037a301b2ebf2574ab13f5d79d0a48)) 75 | * update global and workspace censoring config on create/delete ([4b76cb3](https://github.com/1nVitr0/plugin-vscode-censitive/commit/4b76cb33d1e556e354fdcb6562bdb6e6ebda0aaf)) 76 | * watch global `.censtitive` file in userhome ([2b9126d](https://github.com/1nVitr0/plugin-vscode-censitive/commit/2b9126d6746ebb1f6fa0bccad900f78f735211dc)) 77 | 78 | ## [1.2.5](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.2.4...v1.2.5) (2023-03-22) 79 | 80 | 81 | ### Bug Fixes 82 | 83 | * censor files without active workspace ([0ed489a](https://github.com/1nVitr0/plugin-vscode-censitive/commit/0ed489ae90dff3d0a8a611d9eecd1eda6ea15f14)) 84 | 85 | ## [1.2.5](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.2.4...v1.2.5) (2023-03-22) 86 | 87 | 88 | ### Bug Fixes 89 | 90 | * censor files without active workspace ([0ed489a](https://github.com/1nVitr0/plugin-vscode-censitive/commit/0ed489ae90dff3d0a8a611d9eecd1eda6ea15f14)) 91 | 92 | ## [1.2.4](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.2.3...v1.2.4) (2023-02-15) 93 | 94 | 95 | ### Bug Fixes 96 | 97 | * trigger release ([16dba90](https://github.com/1nVitr0/plugin-vscode-censitive/commit/16dba90439e3da2faa7c3f8c56df1677c067242b)) 98 | 99 | ## [1.2.3](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.2.2...v1.2.3) (2023-02-15) 100 | 101 | 102 | ### Bug Fixes 103 | 104 | * deploy to OpenVSX ([793a3db](https://github.com/1nVitr0/plugin-vscode-censitive/commit/793a3db4ca905df120352b487207239c470c96c1)) 105 | 106 | ## [1.2.3](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.2.2...v1.2.3) (2023-02-15) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * deploy to OpenVSX ([793a3db](https://github.com/1nVitr0/plugin-vscode-censitive/commit/793a3db4ca905df120352b487207239c470c96c1)) 112 | 113 | ## [1.2.2](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.2.1...v1.2.2) (2022-12-21) 114 | 115 | 116 | ### Bug Fixes 117 | 118 | * update svg badges in readme ([4ea9251](https://github.com/1nVitr0/plugin-vscode-censitive/commit/4ea9251c0500af428dcaa33298a949bac812a481)) 119 | 120 | ## [1.2.1](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.2.0...v1.2.1) (2022-12-18) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * allow `//` in `.censitive` files ([3f16364](https://github.com/1nVitr0/plugin-vscode-censitive/commit/3f16364e85accd17fbc66278b44577823dcef88f)) 126 | 127 | # [1.2.0](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.1.4...v1.2.0) (2022-11-15) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * apply global config relative to workspace ([16408d3](https://github.com/1nVitr0/plugin-vscode-censitive/commit/16408d3db3abd9dda32ff248fd31f154c9b573d0)) 133 | 134 | 135 | ### Features 136 | 137 | * use global censoring config in home directory ([5ab9077](https://github.com/1nVitr0/plugin-vscode-censitive/commit/5ab907717c064a242df71b9be881e4f213f317fb)) 138 | 139 | ## [1.1.4](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.1.3...v1.1.4) (2022-06-20) 140 | 141 | 142 | ### Bug Fixes 143 | 144 | * updgrade dependencies ([a4331d1](https://github.com/1nVitr0/plugin-vscode-censitive/commit/a4331d113c0d67f0aef8b52ce6afb6e34a0acef6)) 145 | 146 | ## [1.1.3](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.1.2...v1.1.3) (2022-04-22) 147 | 148 | 149 | ### Bug Fixes 150 | 151 | * fix censoring of values containing `=` ([8c58258](https://github.com/1nVitr0/plugin-vscode-censitive/commit/8c582589e9d0c35d614d8111944eee86cd5159b8)) 152 | * update extension icon ([af66d1c](https://github.com/1nVitr0/plugin-vscode-censitive/commit/af66d1c2ff79635c31c8e23f0eba31d9158582a6)) 153 | 154 | ## [1.1.2](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.1.1...v1.1.2) (2022-04-12) 155 | 156 | 157 | ### Bug Fixes 158 | 159 | * fix non-quoted censoring ([878672a](https://github.com/1nVitr0/plugin-vscode-censitive/commit/878672a6fe63de7005e3cef06cbf5978ca6a6ef6)) 160 | 161 | ## [1.1.1](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.1.0...v1.1.1) (2022-04-10) 162 | 163 | 164 | ### Bug Fixes 165 | 166 | * correctly include escaped quotes ([3246908](https://github.com/1nVitr0/plugin-vscode-censitive/commit/3246908d95f802e7c2c63cc0b06e425867a89062)) 167 | * display line censoring index in code lenses ([0336b70](https://github.com/1nVitr0/plugin-vscode-censitive/commit/0336b706ccc924b2b7aeeb3c7491fd26169a78a9)) 168 | 169 | 170 | ### Performance Improvements 171 | 172 | * increase caching performance ([6026dab](https://github.com/1nVitr0/plugin-vscode-censitive/commit/6026dab0755eb0f842b7e09301344e474d7c46ec)) 173 | 174 | # Changelog 175 | 176 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 177 | 178 | ## [1.1.0](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.0.2...v1.1.0) (2021-12-17) 179 | 180 | 181 | ### Features 182 | 183 | * allow censoring of entire files ([fd4d60b](https://github.com/1nVitr0/plugin-vscode-censitive/commit/fd4d60bb1a43e71d8adcc1b089fd70cea4e1f647)) 184 | * censor multiline values ([8c1418b](https://github.com/1nVitr0/plugin-vscode-censitive/commit/8c1418b7dd1dc58e52a0a62f6d1a406603d3acbc)) 185 | 186 | 187 | ### Bug Fixes 188 | 189 | * fix caching on multiline edits ([a5377f5](https://github.com/1nVitr0/plugin-vscode-censitive/commit/a5377f59855719d330a0c0bf672e072c64c25d6b)) 190 | 191 | ### [1.0.2](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.0.1...v1.0.2) (2021-12-17) 192 | 193 | 194 | ### Bug Fixes 195 | 196 | * updat documentation and name ([a066980](https://github.com/1nVitr0/plugin-vscode-censitive/commit/a0669806dbd7f7b1dd617da751e4d5e23f22d58d)) 197 | 198 | ### [1.0.1](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v1.0.0...v1.0.1) (2021-12-17) 199 | 200 | 201 | ### Bug Fixes 202 | 203 | * fix inline censoring ([9647fd4](https://github.com/1nVitr0/plugin-vscode-censitive/commit/9647fd4e917555e5a4af645a5dfd94e576f010f5)) 204 | 205 | ## [1.0.0](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v0.1.0...v1.0.0) (2021-04-21) 206 | 207 | 208 | ### Features 209 | 210 | * add code actions for copying and showing ([9c9b401](https://github.com/1nVitr0/plugin-vscode-censitive/commit/9c9b4010c07aa0cb3d42977b0d25424023ec0050)) 211 | * add configuration for display time ([d4cd205](https://github.com/1nVitr0/plugin-vscode-censitive/commit/d4cd20514a5ea4929b6dc81b1d71a113735b46cc)) 212 | 213 | ## [0.1.0](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v0.0.4...v0.1.0) (2021-03-15) 214 | 215 | 216 | ### ⚠ BREAKING CHANGES 217 | 218 | * removed opacity settings for censoring 219 | * languages option no longer available, combined censoring types 220 | 221 | ### Features 222 | 223 | * allow theme values ([dc72017](https://github.com/1nVitr0/plugin-vscode-censitive/commit/dc720179f249b2d9a9e4d912a188ee3798db836a)) 224 | * fast(ish) censoring for large documents ([b748e20](https://github.com/1nVitr0/plugin-vscode-censitive/commit/b748e20d735a6a06afe8b50df7730b1698540dbe)) 225 | 226 | 227 | * clean up configuration ([334d051](https://github.com/1nVitr0/plugin-vscode-censitive/commit/334d051a13be9798cbf6b478137e1885fca64060)) 228 | 229 | ### [0.0.4](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v0.0.3...v0.0.4) (2021-02-18) 230 | 231 | ### [0.0.3](https://github.com/1nVitr0/plugin-vscode-censitive/compare/v0.0.2...v0.0.3) (2021-02-18) 232 | 233 | 234 | ### Features 235 | 236 | * :sparkler: allow comments in .censitive file ([c74bcb6](https://github.com/1nVitr0/plugin-vscode-censitive/commit/c74bcb64b0fc196f6fdfd5c85b94ce0cb7611ba7)) 237 | 238 | ### 0.0.2 (2021-02-18) 239 | 240 | 241 | ### Features 242 | 243 | * :sparkles: add syntax highlighting ([57621c3](https://github.com/1nVitr0/plugin-vscode-censitive/commit/57621c303e442535e5a128ddd9655fc0356bbd03)) 244 | * add icon ([6531358](https://github.com/1nVitr0/plugin-vscode-censitive/commit/653135867bf03e0828a295220ca890f29fdc31a3)) 245 | 246 | 247 | ### Bug Fixes 248 | 249 | * :bug: dont apply ranges twice on extension install ([2e4d63f](https://github.com/1nVitr0/plugin-vscode-censitive/commit/2e4d63f47b5662080dccf8690ddb01f897fe416c)) 250 | * :pencil: fix image visibility in readme ([258a99b](https://github.com/1nVitr0/plugin-vscode-censitive/commit/258a99b8a0ce0c3bb41cddbc447a78b0654c2a77)) 251 | 252 | ## 0.0.1 253 | 254 | - Initial release 255 | -------------------------------------------------------------------------------- /src/providers/CensoringProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Disposable, 3 | Position, 4 | Range, 5 | TextDocument, 6 | TextDocumentContentChangeEvent, 7 | TextEditorDecorationType, 8 | TextLine, 9 | window, 10 | workspace, 11 | } from "vscode"; 12 | import CensorBar, { CensorOptions } from "../decorations/CensorBar"; 13 | import CensoringCodeLensProvider from "./CensoringCodeLensProvider"; 14 | import ConfigurationProvider, { FencingPattern } from "./ConfigurationProvider"; 15 | 16 | type RegexKeyValueParts = { key: string; assignment: string; value: string }; 17 | type MultilineCensor = { 18 | start(line: TextLine, regexParts: RegexKeyValueParts): number | null; 19 | end(line: TextLine, keyIndent: number, regexParts: RegexKeyValueParts): number | null; 20 | }; 21 | 22 | const pythonLikeMultiline: MultilineCensor = { 23 | start: (line: TextLine, regex) => 24 | line.text.match(new RegExp(`${regex.key}${regex.assignment}"""`, "i"))?.[0].length ?? null, 25 | end: (line: TextLine) => (line.text.indexOf('"""') >= 0 ? line.text.indexOf('"""') : null), 26 | }; 27 | 28 | export default class CensoringProvider { 29 | public readonly document: TextDocument; 30 | 31 | private static multilineValues: Record = { 32 | python: pythonLikeMultiline, 33 | toml: pythonLikeMultiline, 34 | yaml: { 35 | start: (line: TextLine, regex) => 36 | line.text.match(new RegExp(`${regex.key}\\s*:\\s*[>|]-?\\s*`, "i"))?.[0].length ? Infinity : null, 37 | end: (line: TextLine, indent) => (line.firstNonWhitespaceCharacterIndex <= indent ? -1 : null), 38 | }, 39 | }; 40 | 41 | private documentVersion: number = -1; 42 | private _disposed: boolean = false; 43 | private censoredRanges: Range[] = []; 44 | private visibleRanges: Range[] = []; 45 | private censorBar: CensorBar; 46 | private codeLensProvider: CensoringCodeLensProvider; 47 | private codeLensDisposable?: Disposable; 48 | private configurationProvider = new ConfigurationProvider(); 49 | private listeners: Disposable[] = []; 50 | 51 | public static buildCensorKeyRegex( 52 | keys: string[], 53 | assignment: string, 54 | ...additionalValueExpressions: string[] 55 | ): RegexKeyValueParts { 56 | const escapedKeys = keys.map((key) => key.replace(/(? this.onUpdate(document, null, contentChanges)) 82 | ); 83 | } 84 | 85 | public get disposed() { 86 | return this._disposed; 87 | } 88 | 89 | public getCensorRegex(keys: string[], languageId?: string): RegExp { 90 | const { config } = this.configurationProvider; 91 | const isCodeLanguage = languageId && config.codeLanguages.indexOf(languageId) > -1; 92 | const assignmentRegex = config.assignmentRegex[languageId ?? "default"] || config.assignmentRegex.default; 93 | const additionalValues = isCodeLanguage ? [] : ["([^\\s\\v\\r\\n,;]*)"]; 94 | 95 | const { key, assignment, value } = CensoringProvider.buildCensorKeyRegex( 96 | keys, 97 | assignmentRegex, 98 | ...additionalValues 99 | ); 100 | 101 | return new RegExp(`(['"]?${key}['"]?${assignment})${value}(?:[\\s\\r\\n,;]|$)`, "gi"); 102 | } 103 | 104 | public addVisibleRange(range: Range): void { 105 | this.visibleRanges.push(range); 106 | } 107 | 108 | public removeVisibleRange(range: Range): void { 109 | const i = this.visibleRanges.findIndex((compare) => range.isEqual(compare)); 110 | if (i >= 0) { 111 | this.visibleRanges.splice(i, 1); 112 | } 113 | } 114 | 115 | public clearVisibleRanges(): void { 116 | this.visibleRanges = []; 117 | } 118 | 119 | public dispose() { 120 | this._disposed = true; 121 | this.listeners.forEach((listener) => listener.dispose()); 122 | this.codeLensDisposable?.dispose(); 123 | this.censorBar.decoration.dispose(); 124 | } 125 | 126 | public async censor(fast = false, configChanged = false) { 127 | const { config } = this.configurationProvider; 128 | 129 | // We need to reapply the decorations when the visibility changes 130 | if ( 131 | this.document.version === this.documentVersion && 132 | this.configurationProvider.isDocumentInWorkspace(this.document) && 133 | !configChanged 134 | ) { 135 | return this.applyCensoredRanges(); 136 | } 137 | 138 | const keys = await this.configurationProvider.getCensoredKeys(this.document); 139 | if (keys.includes("*")) { 140 | this.censoredRanges = [this.document.validateRange(new Range(0, 0, this.document.lineCount, Infinity))]; 141 | this.documentVersion = this.document.version; 142 | return this.applyCensoredRanges(); 143 | } 144 | 145 | const { uri, lineCount } = this.document; 146 | const visibleEditors = window.visibleTextEditors.filter( 147 | ({ document }) => document.uri.toString() === uri.toString() 148 | ); 149 | const visibleRanges = visibleEditors.reduce((ranges, editor) => [...ranges, ...editor.visibleRanges], []); 150 | 151 | if (fast && lineCount > config.useFastModeMinLines) { 152 | await this.onUpdate(this.document, visibleRanges, configChanged); 153 | } 154 | await this.onUpdate(this.document, null, configChanged); 155 | 156 | this.documentVersion = this.document.version; 157 | } 158 | 159 | public applyCensoredRanges() { 160 | const { document, censorBar, visibleRanges, censoredRanges } = this; 161 | 162 | const removePrevious = censorBar.regenerateDecoration(); 163 | this.applyDecoration( 164 | censorBar.decoration, 165 | censoredRanges.filter((range) => !visibleRanges.some((visible) => visible.isEqual(range))) 166 | ); 167 | this.codeLensDisposable = this.codeLensProvider.setCensoredRanges(document, censoredRanges, visibleRanges); 168 | if (removePrevious) { 169 | removePrevious(); 170 | } 171 | } 172 | 173 | private async updateCensoredRanges(text: string, version: number, offset?: Position): Promise { 174 | if (this.document.version !== version) { 175 | throw new Error("Document version has already changed"); 176 | } 177 | 178 | const changes = await this.getCensoredRanges(text, offset); 179 | this.censoredRanges.push(...changes); 180 | 181 | return changes.length; 182 | } 183 | 184 | private async updateMultilineCensoredRanges(version: number): Promise { 185 | if (this.document.version !== version) { 186 | throw new Error("Document version has already changed"); 187 | } 188 | 189 | for (let i = this.censoredRanges.length - 1; i > 0; i--) { 190 | const range = this.censoredRanges[i]; 191 | if (!range.isSingleLine) { 192 | this.censoredRanges.splice(i, 1); 193 | } 194 | } 195 | 196 | const newRanges = await this.getMultilineRanges(); 197 | this.censoredRanges.push(...newRanges); 198 | 199 | return newRanges.length; 200 | } 201 | 202 | private async onUpdate( 203 | document = this.document, 204 | ranges?: Range[] | null, 205 | contentChanges?: readonly TextDocumentContentChangeEvent[] | boolean 206 | ) { 207 | const { version, uri } = document; 208 | if ( 209 | this.disposed || 210 | uri.toString() !== this.document.uri.toString() || 211 | (version === this.documentVersion && contentChanges !== true) 212 | ) { 213 | return; 214 | } 215 | 216 | const keys = await this.configurationProvider.getCensoredKeys(document); 217 | if (keys.includes("*")) { 218 | this.documentVersion = version; 219 | return this.censor(false, contentChanges === true); 220 | } 221 | 222 | const promises: Promise[] = []; 223 | let deletions = 0; 224 | 225 | if (contentChanges === true) { 226 | this.censoredRanges = []; 227 | promises.push(this.updateCensoredRanges(document.getText(), version)); 228 | } else if (ranges) { 229 | for (const range of ranges) { 230 | promises.push(this.updateCensoredRanges(document.getText(range), version, range.start)); 231 | } 232 | } else if (contentChanges) { 233 | let lineOffset = 0; 234 | for (const change of contentChanges) { 235 | // Expand range to re-evaluate incomplete censor key-value pairs 236 | const { range, text } = change.range.isSingleLine ? document.lineAt(change.range.start.line) : change; 237 | for (let i = this.censoredRanges.length - 1; i >= 0; i--) { 238 | if (range.intersection(this.censoredRanges[i])) { 239 | this.censoredRanges.splice(i, 1); 240 | deletions++; 241 | } 242 | } 243 | promises.push(this.updateCensoredRanges(text, version, range.start.translate(lineOffset))); 244 | lineOffset += change.text.split("\n").length - (range.end.line - range.start.line + 1); 245 | for (let i = 0; i < this.censoredRanges.length; i++) { 246 | const range = this.censoredRanges[i]; 247 | if (range.start.line > change.range.end.line) { 248 | this.censoredRanges[i] = new Range(range.start.translate(lineOffset), range.end.translate(lineOffset)); 249 | } 250 | } 251 | } 252 | } else { 253 | this.censoredRanges = []; 254 | promises.push(this.updateCensoredRanges(document.getText(), version)); 255 | } 256 | 257 | const changes = await Promise.all(promises); 258 | if (contentChanges === true || deletions || changes.reduce((sum, n) => sum + n, 0) > 0) { 259 | this.applyCensoredRanges(); 260 | } 261 | 262 | const multilineChanges = await this.updateMultilineCensoredRanges(version); 263 | if (multilineChanges) { 264 | this.applyCensoredRanges(); 265 | } 266 | 267 | this.documentVersion = version; 268 | } 269 | 270 | private applyDecoration(decoration: TextEditorDecorationType, ranges: Range[]) { 271 | window.visibleTextEditors 272 | .filter(({ document }) => document.uri.toString() === this.document.uri.toString()) 273 | .forEach((editor) => editor.setDecorations(decoration, ranges)); 274 | } 275 | 276 | private async getCensoredRanges(text: string, offset?: Position): Promise { 277 | const { languageId } = this.document; 278 | const keys = (await this.configurationProvider.getCensoredKeys(this.document)).filter( 279 | (key) => typeof key === "string" 280 | ) as string[]; 281 | if (!keys.length) { 282 | return []; 283 | } 284 | 285 | const documentOffset = offset ? this.document.offsetAt(offset) : 0; 286 | const ranges: Range[] = []; 287 | const regex = this.getCensorRegex(keys, languageId); 288 | 289 | let currentMatch = regex.exec(text); 290 | while (currentMatch !== null) { 291 | const [_, key, value, ...innerAll] = currentMatch; 292 | const inner = innerAll.reduce((max, s) => (s && s.length > max.length ? s : max), ""); 293 | 294 | const valueOffset = inner ? value?.indexOf(inner) : 0; 295 | const start = currentMatch.index + key.length + valueOffset; 296 | const end = start + (inner?.length ?? 0); 297 | 298 | ranges.push( 299 | new Range(this.document.positionAt(start + documentOffset), this.document.positionAt(end + documentOffset)) 300 | ); 301 | 302 | currentMatch = regex.exec(text); 303 | } 304 | 305 | return ranges; 306 | } 307 | 308 | public async getMultilineRanges() { 309 | const { languageId } = this.document; 310 | const censoring = await this.configurationProvider.getCensoredKeys(this.document); 311 | const keys = censoring.filter((key) => typeof key === "string") as string[]; 312 | const fencePatterns = censoring.filter((key) => typeof key !== "string") as FencingPattern[]; 313 | 314 | const ranges = []; 315 | let multiline = CensoringProvider.multilineValues[languageId]; 316 | const regexParts = CensoringProvider.buildCensorKeyRegex(keys, languageId); 317 | 318 | if (fencePatterns.length > 0) { 319 | const regexpStart = new RegExp(fencePatterns.map((f) => `(${f.start})`).join("|")); 320 | const regexpEnd = new RegExp(fencePatterns.map((f) => `(${f.end})`).join("|")); 321 | 322 | const { start = undefined, end = undefined } = multiline ?? {}; 323 | 324 | multiline = { 325 | start(line: TextLine, regex) { 326 | const match = regexpStart.exec(line.text); 327 | return match ? match.index + match[0].length : start?.(line, regex) ?? null; 328 | }, 329 | end(line: TextLine, indent, regex) { 330 | const match = regexpEnd.exec(line.text); 331 | return match ? match.index : end?.(line, indent, regex) ?? null; 332 | }, 333 | }; 334 | } 335 | 336 | if (multiline) { 337 | let start: Position | null = null; 338 | let startIndent = -1; 339 | let line = this.document.lineAt(0); 340 | while (line.lineNumber < this.document.lineCount) { 341 | const startOffset = multiline.start(line, regexParts); 342 | if (start === null && startOffset !== null) { 343 | start = this.getLineOffset(line, startOffset); 344 | startIndent = line.firstNonWhitespaceCharacterIndex; 345 | } else if (start !== null) { 346 | const endOffset = multiline.end(line, startIndent, regexParts); 347 | if (endOffset !== null) { 348 | ranges.push(this.document.validateRange(new Range(start, this.getLineOffset(line, endOffset)))); 349 | start = null; 350 | } 351 | } 352 | 353 | if (line.lineNumber === this.document.lineCount - 1) { 354 | break; 355 | } 356 | line = this.document.lineAt(line.lineNumber + 1); 357 | } 358 | 359 | if (start !== null) { 360 | ranges.push(this.document.validateRange(new Range(start, line.range.end))); 361 | } 362 | } 363 | 364 | return ranges; 365 | } 366 | 367 | public getLineOffset(line: TextLine, offset: number): Position { 368 | if (offset === -1) { 369 | return new Position(line.lineNumber - 1, Infinity); 370 | } else if (offset === Infinity) { 371 | return new Position(line.lineNumber + 1, 0); 372 | } else { 373 | return new Position(line.lineNumber, offset); 374 | } 375 | } 376 | } 377 | -------------------------------------------------------------------------------- /src/providers/ConfigurationProvider.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DocumentSelector, 3 | env, 4 | FileStat, 5 | languages, 6 | RelativePattern, 7 | TextDocument, 8 | Uri, 9 | window, 10 | workspace, 11 | WorkspaceConfiguration, 12 | WorkspaceFolder, 13 | } from "vscode"; 14 | import { CensorOptions } from "../decorations/CensorBar"; 15 | import { dirname, relative, isAbsolute } from "path"; 16 | 17 | export type DefaultCensoringConfig = 18 | | { 19 | match: string; 20 | exclude?: string; 21 | censor: (string | { start: string; end: string })[]; 22 | } 23 | | { exclude: string }; 24 | 25 | export interface Configuration { 26 | censoring: CensorOptions; 27 | showTimeoutSeconds: number; 28 | useFastModeMinLines: number; 29 | enable: boolean; 30 | mergeGlobalCensoring: boolean; 31 | codeLanguages: string[]; 32 | assignmentRegex: Record & { default: string }; 33 | defaultCensoring: DefaultCensoringConfig[]; 34 | mergeDefaultCensoring?: boolean; 35 | } 36 | 37 | export interface FencingPattern { 38 | start: string; 39 | end: string; 40 | } 41 | 42 | export type CensoringKeysBase = { 43 | keys: string[]; 44 | fencingPatterns?: FencingPattern[]; 45 | selector: DocumentSelector | null; 46 | exclude?: DocumentSelector; 47 | }; 48 | 49 | export interface CensoringKeysWithSelector extends CensoringKeysBase { 50 | selector: DocumentSelector; 51 | } 52 | 53 | export interface CensoringKeysIgnore extends CensoringKeysBase { 54 | selector: null; 55 | exclude: DocumentSelector; 56 | } 57 | 58 | export type CensoringKeys = CensoringKeysWithSelector | CensoringKeysIgnore; 59 | 60 | export const defaults: Configuration = { 61 | enable: true, 62 | mergeGlobalCensoring: true, 63 | useFastModeMinLines: 10000, 64 | showTimeoutSeconds: 10, 65 | codeLanguages: [ 66 | "coffeescript", 67 | "c", 68 | "cpp", 69 | "csharp", 70 | "fsharp", 71 | "go", 72 | "groovy", 73 | "handlebars", 74 | "html", 75 | "java", 76 | "javascript", 77 | "lua", 78 | "objective-c", 79 | "objective-cpp", 80 | "perl", 81 | "php", 82 | "jade", 83 | "pug", 84 | "python", 85 | "r", 86 | "razor", 87 | "ruby", 88 | "rust", 89 | "slim", 90 | "typescript", 91 | "vb", 92 | "vue", 93 | "vue-html", 94 | ], 95 | assignmentRegex: { 96 | default: "[\\t ]*[:=][=>]?[\\t ]*", 97 | yaml: "[\\t ]*:[\\t ]*(?!>|\\|)", 98 | cpp: "(?:[\\t ]*=[\\t ]*|(?<=^#define[\\t ]+.+)[\\t ]+)", 99 | c: "(?:[\\t ]*=[\\t ]*|(?<=^#define[\\t ]+.+)[\\t ]+)", 100 | }, 101 | censoring: { 102 | color: "theme.editorInfo.background", 103 | prefix: "🔒", 104 | border: "2px solid grey", 105 | grow: true, 106 | }, 107 | defaultCensoring: [ 108 | { 109 | match: "**/{env,.env,env.*,.env.*}", 110 | exclude: "**/{env.example,.env.example}", 111 | censor: [".*password", ".*token", ".*secret.*"], 112 | }, 113 | { 114 | match: "**/id_{rsa,dsa,ecdsa,eddsa,dss,sha2}", 115 | censor: [{ start: "-----BEGIN.*PRIVATE KEY-----", end: "-----END.*PRIVATE KEY-----" }], 116 | }, 117 | ], 118 | }; 119 | 120 | function isWorkspaceFolder(folder: WorkspaceFolder | Uri): folder is WorkspaceFolder { 121 | return (folder as WorkspaceFolder).uri !== undefined; 122 | } 123 | 124 | function isCensoringKeyWithSelector(key: CensoringKeys): key is CensoringKeysWithSelector { 125 | return key.selector !== null; 126 | } 127 | 128 | function isCensoringKeyIgnore(key: CensoringKeys): key is CensoringKeysIgnore { 129 | return key.selector === null; 130 | } 131 | 132 | export default class ConfigurationProvider { 133 | public static censorKeys: { [workspace: string]: CensoringKeys[] } = {}; 134 | private static _config: WorkspaceConfiguration; 135 | private static _globalWorkspaceName = "global"; 136 | private static _defaultWorkspaceName = "default"; 137 | private static _userHome = env.appRoot; 138 | 139 | public static async init() { 140 | if (ConfigurationProvider._userHome) { 141 | try { 142 | // Try to set the correct user home path 143 | ConfigurationProvider._userHome = require("os").homedir(); 144 | } catch (e) { 145 | window.showErrorMessage(".censitive could not get user home directory, global config will not be loaded"); 146 | } 147 | } 148 | await this.updateConfig(); 149 | await this.loadCensoringConfigFiles(); 150 | } 151 | 152 | public static async updateConfig() { 153 | ConfigurationProvider._config = workspace.getConfiguration("censitive"); 154 | ConfigurationProvider.censorKeys[ConfigurationProvider._defaultWorkspaceName] = ( 155 | ((ConfigurationProvider._config as unknown) || defaults) as Configuration 156 | ).defaultCensoring?.map(({ exclude, ...options }) => { 157 | return "match" in options 158 | ? { 159 | selector: { pattern: options.match }, 160 | exclude: exclude && { pattern: exclude }, 161 | keys: options.censor.filter((key): key is string => typeof key === "string"), 162 | fencingPatterns: options.censor.filter((key): key is FencingPattern => typeof key !== "string"), 163 | } 164 | : { selector: null, exclude: { pattern: exclude! }, keys: [] }; 165 | }); 166 | } 167 | 168 | public static async updateCensoringKeys( 169 | workspaceFolder: WorkspaceFolder | Uri | null, 170 | configFile: Uri | null, 171 | base: Uri | WorkspaceFolder | null = workspaceFolder 172 | ) { 173 | const name = base ? (isWorkspaceFolder(base) ? base.name : base.path) : ConfigurationProvider._globalWorkspaceName; 174 | if (!configFile) { 175 | return (ConfigurationProvider.censorKeys[name] = []); 176 | } 177 | 178 | try { 179 | const content = await workspace.fs.readFile(configFile); 180 | return (ConfigurationProvider.censorKeys[name] = content 181 | .toString() 182 | .split(/\r?\n/g) 183 | .filter((line) => line.trim() && !line.startsWith("#") && !line.startsWith("//")) 184 | .map((line) => { 185 | const [patternRaw, keyList = "", fenceList] = line.split(/(? !!key); 202 | const fencingPatterns = fenceList 203 | ? fenceList 204 | .replace(/\\:/g, ":") 205 | .split(/,\s*/g) 206 | .map((fence) => ({ 207 | start: keys.shift() || "", 208 | end: fence, 209 | })) 210 | .filter(({ start }) => !!start) 211 | : undefined; 212 | return { selector, exclude, keys, fencingPatterns }; 213 | })); 214 | } catch (e) { 215 | window.showErrorMessage("Failed to load censitive config (see console for more info)"); 216 | console.error(e); 217 | return (ConfigurationProvider.censorKeys[name] = []); 218 | } 219 | } 220 | 221 | private static async loadCensoringConfigFiles(path?: Uri) { 222 | const { stat } = workspace.fs; 223 | const workspaces = workspace.workspaceFolders || []; 224 | const insideWorkspace = 225 | path && 226 | workspaces.some(({ uri }) => { 227 | const relativePath = relative(uri.fsPath, path.fsPath); 228 | return relativePath && !relativePath.startsWith("..") && !isAbsolute(relativePath); 229 | }); 230 | 231 | if (path && insideWorkspace) { 232 | // Workspace files are watched and updated on change 233 | return; 234 | } else if (path) { 235 | // We must reload the configuration for non-workspace directories 236 | // They are unwatched and opened on demand, caching would be inefficient 237 | const parentDirectories = ConfigurationProvider.getParentDirectories(path); 238 | 239 | await Promise.all( 240 | parentDirectories.map(async (directory) => { 241 | const censitiveFile = Uri.joinPath(directory, ".censitive"); 242 | const { size } = await new Promise((resolve, reject) => 243 | stat(censitiveFile).then(resolve, reject) 244 | ).catch(() => ({ size: 0 })); 245 | 246 | if (size > 0) { 247 | await ConfigurationProvider.updateCensoringKeys(null, censitiveFile, directory); 248 | } 249 | }) 250 | ); 251 | return; 252 | } 253 | 254 | for (const folder of workspaces) { 255 | const configFiles = await workspace.findFiles(new RelativePattern(folder, "**/.censitive")); 256 | await Promise.all( 257 | configFiles.map((censitiveUri) => { 258 | const base = Uri.file(dirname(censitiveUri.fsPath)); 259 | return ConfigurationProvider.updateCensoringKeys( 260 | folder, 261 | censitiveUri, 262 | base.toString() === folder.uri.toString() ? folder : base 263 | ); 264 | }) 265 | ); 266 | } 267 | 268 | if (ConfigurationProvider._userHome) { 269 | const globalConfigFile = Uri.joinPath(Uri.file(ConfigurationProvider._userHome), ".censitive"); 270 | const { size } = await new Promise((resolve, reject) => 271 | stat(globalConfigFile).then(resolve, reject) 272 | ).catch(() => ({ size: 0 })); 273 | if (size > 0) { 274 | await ConfigurationProvider.updateCensoringKeys(null, globalConfigFile); 275 | } 276 | } 277 | } 278 | 279 | private static getParentDirectories(path: string | Uri, ignoreHome = false) { 280 | const parentDirectories = []; 281 | const userHomePath = Uri.file(ConfigurationProvider._userHome).fsPath; 282 | 283 | let currentDirectory = typeof path === "string" ? path : path.fsPath; 284 | while (currentDirectory !== dirname(currentDirectory)) { 285 | if (!ignoreHome || currentDirectory !== userHomePath) { 286 | parentDirectories.push(Uri.file(currentDirectory)); 287 | } 288 | currentDirectory = dirname(currentDirectory); 289 | } 290 | 291 | return parentDirectories; 292 | } 293 | 294 | public get userHome(): string { 295 | return ConfigurationProvider._userHome; 296 | } 297 | 298 | public get globalCensorKeys(): CensoringKeys[] { 299 | return ConfigurationProvider.censorKeys[ConfigurationProvider._globalWorkspaceName] ?? []; 300 | } 301 | 302 | public get defaultCensorKeys(): CensoringKeys[] { 303 | return ConfigurationProvider.censorKeys[ConfigurationProvider._defaultWorkspaceName] ?? []; 304 | } 305 | 306 | public get hasGlobalCensorKeys(): boolean { 307 | return !!this.globalCensorKeys; 308 | } 309 | 310 | public get hasDefaultCensorKeys(): boolean { 311 | return !!this.defaultCensorKeys; 312 | } 313 | 314 | public get censorOptions(): CensorOptions { 315 | if (typeof ConfigurationProvider._config?.censoring === "object") { 316 | return { ...ConfigurationProvider._config?.censoring } as CensorOptions; 317 | } 318 | 319 | return defaults.censoring; 320 | } 321 | 322 | public toggleEnable() { 323 | const enabled = ConfigurationProvider._config?.get("enable"); 324 | ConfigurationProvider._config?.update("enable", !enabled); 325 | 326 | return !enabled; 327 | } 328 | 329 | public get config(): Configuration { 330 | return ((ConfigurationProvider._config as unknown) || defaults) as Configuration; 331 | } 332 | 333 | public static isCensoringKeyEqual(a: CensoringKeys, b: CensoringKeys) { 334 | return ( 335 | a.selector === b.selector && 336 | a.exclude === b.exclude && 337 | a.keys.every((key) => b.keys.includes(key)) && 338 | b.keys.every((key) => a.keys.includes(key)) 339 | ); 340 | } 341 | 342 | public async isDocumentInCensorConfig(document: TextDocument): Promise { 343 | const workspaceFolder = workspace.getWorkspaceFolder(document.uri); 344 | const parentFolder = Uri.file(dirname(document.uri.fsPath)); 345 | const allCensorKeys = await this.getCensoringKeysForFolder(workspaceFolder, parentFolder); 346 | const censorKeys = allCensorKeys.filter(isCensoringKeyWithSelector); 347 | const ignoreKeys = allCensorKeys.filter(isCensoringKeyIgnore); 348 | 349 | if (censorKeys.length === 0) { 350 | return false; 351 | } 352 | 353 | const { match } = languages; 354 | 355 | if (ignoreKeys.some(({ exclude = "" }) => match(exclude, document) > 0)) { 356 | return false; 357 | } 358 | 359 | for (const { selector, exclude } of censorKeys) { 360 | if (exclude && match(exclude, document) > 0) { 361 | continue; 362 | } else if (match(selector, document) > 0) { 363 | return true; 364 | } 365 | } 366 | 367 | return false; 368 | } 369 | 370 | public isDocumentInWorkspace(document: TextDocument): boolean { 371 | const workspaceFolder = workspace.getWorkspaceFolder(document.uri); 372 | return !!workspaceFolder; 373 | } 374 | 375 | public async getCensoredKeys(document: TextDocument): Promise<(string | FencingPattern)[]> { 376 | const workspaceFolder = workspace.getWorkspaceFolder(document.uri); 377 | const parentFolder = Uri.file(dirname(document.uri.fsPath)); 378 | const censorKeys = await this.getCensoringKeysForFolder(workspaceFolder, parentFolder); 379 | 380 | if (censorKeys.length === 0) { 381 | return []; 382 | } 383 | 384 | return censorKeys.reduce<(string | FencingPattern)[]>((acc, { selector, keys, fencingPatterns = [] }) => { 385 | return selector && languages.match(selector, document) > 0 ? [...acc, ...keys, ...fencingPatterns] : acc; 386 | }, []); 387 | } 388 | 389 | private async getCensoringKeysForFolder(base?: WorkspaceFolder, subPath?: Uri): Promise { 390 | const { mergeGlobalCensoring, mergeDefaultCensoring } = this.config; 391 | 392 | const baseCensoringKeys = base ? ConfigurationProvider.censorKeys[base.name] : undefined; 393 | const censorKeyGroups = []; 394 | 395 | if (subPath) { 396 | if (!base) { 397 | // Load config for files outside the workspace 398 | await ConfigurationProvider.loadCensoringConfigFiles(subPath); 399 | } 400 | 401 | const parentDirectories = ConfigurationProvider.getParentDirectories(subPath); 402 | for (const directory of parentDirectories) { 403 | if (ConfigurationProvider.censorKeys[directory.path]) { 404 | censorKeyGroups.push(ConfigurationProvider.censorKeys[directory.path]); 405 | } 406 | } 407 | } 408 | 409 | if ((mergeGlobalCensoring || !baseCensoringKeys) && this.hasGlobalCensorKeys) { 410 | censorKeyGroups.push(this.globalCensorKeys); 411 | } 412 | if (mergeDefaultCensoring || (!baseCensoringKeys && !this.hasGlobalCensorKeys)) { 413 | censorKeyGroups.push(this.defaultCensorKeys); 414 | } 415 | 416 | const censorKeys = this.mergeCensoringKeys(baseCensoringKeys ?? [], ...censorKeyGroups); 417 | 418 | return censorKeys.map((censorKey) => { 419 | let { selector, exclude } = censorKey; 420 | selector = selector && this.extendGlobSelector(selector, base); 421 | exclude = exclude && this.extendGlobSelector(exclude, base); 422 | 423 | return { ...censorKey, selector, exclude } as CensoringKeys; 424 | }); 425 | } 426 | 427 | private mergeCensoringKeys(base: CensoringKeys[], ...merge: CensoringKeys[][]) { 428 | const censorKeys = [...base]; 429 | for (const keys of merge) { 430 | censorKeys.push( 431 | ...keys.filter((censorKey) => 432 | censorKeys.every((key) => !ConfigurationProvider.isCensoringKeyEqual(key, censorKey)) 433 | ) 434 | ); 435 | } 436 | 437 | return censorKeys; 438 | } 439 | 440 | private extendGlobSelector(selector: DocumentSelector, folder?: WorkspaceFolder) { 441 | if (typeof selector === "string") { 442 | const dirGlob = selector.startsWith("**") || selector.startsWith("./**") ? "" : "**/"; 443 | return folder ? new RelativePattern(folder, selector) : `${dirGlob}${selector}`; 444 | } else { 445 | return selector; 446 | } 447 | } 448 | } 449 | --------------------------------------------------------------------------------