├── .gitignore ├── misc ├── icon.png ├── logo.png ├── screenshot-after.png └── screenshot-before.png ├── .vscodeignore ├── extension.js ├── .vscode ├── extensions.json └── launch.json ├── jsconfig.json ├── test ├── suite │ ├── extension.test.js │ └── index.js └── runTest.js ├── .eslintrc.json ├── README.md ├── LICENSE ├── package.json └── script └── byesig.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test/ 3 | *.vsix 4 | -------------------------------------------------------------------------------- /misc/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itarato/byesig/HEAD/misc/icon.png -------------------------------------------------------------------------------- /misc/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itarato/byesig/HEAD/misc/logo.png -------------------------------------------------------------------------------- /misc/screenshot-after.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itarato/byesig/HEAD/misc/screenshot-after.png -------------------------------------------------------------------------------- /misc/screenshot-before.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/itarato/byesig/HEAD/misc/screenshot-before.png -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | vsc-extension-quickstart.md 6 | **/jsconfig.json 7 | **/*.map 8 | **/.eslintrc.json 9 | -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | const byesig = require('./script/byesig.js'); 2 | 3 | exports.activate = byesig.activate; 4 | module.exports = { 5 | activate: byesig.activate, 6 | deactivate: function() {}, 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "checkJs": true, /* Typecheck .js files. */ 6 | "lib": [ 7 | "es6" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /test/suite/extension.test.js: -------------------------------------------------------------------------------- 1 | const assert = require('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 | const vscode = require('vscode'); 6 | // const myExtension = require('../extension'); 7 | 8 | suite('Extension Test Suite', () => { 9 | vscode.window.showInformationMessage('Start all tests.'); 10 | 11 | test('Sample test', () => { 12 | assert.equal(-1, [1, 2, 3].indexOf(5)); 13 | assert.equal(-1, [1, 2, 3].indexOf(0)); 14 | }); 15 | }); 16 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true, 7 | "mocha": true 8 | }, 9 | "parserOptions": { 10 | "ecmaVersion": 2018, 11 | "ecmaFeatures": { 12 | "jsx": true 13 | }, 14 | "sourceType": "module" 15 | }, 16 | "rules": { 17 | "no-const-assign": "warn", 18 | "no-this-before-super": "warn", 19 | "no-undef": "warn", 20 | "no-unreachable": "warn", 21 | "no-unused-vars": "warn", 22 | "constructor-super": "warn", 23 | "valid-typeof": "warn" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ByeSig 2 | 3 | ByeSig is a VSCode plugin to temporarily hide [Sorbet](https://sorbet.org/) signatures in Ruby source files. 4 | 5 | When active it fades and folds Sorbet `sig` lines and blocks: 6 | 7 | Before: 8 | ![Screenshot before](misc/screenshot-before.png) 9 | 10 | After: 11 | ![Screenshot after](misc/screenshot-after.png) 12 | 13 | # Options 14 | 15 | - VSCode commands: force show and hide 16 | - VSCode configuration: 17 | - opacity 18 | - backgroundColor 19 | - plugin on/off 20 | - fold on/off 21 | - gutter icon on/off 22 | 23 | # Contribution 24 | 25 | Still super beta, any feedback / issue / pull request is welcomed. 26 | -------------------------------------------------------------------------------- /test/runTest.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const { runTests } = require('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 the extension test script 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/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "runtimeExecutable": "${execPath}", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ] 16 | }, 17 | { 18 | "name": "Extension Tests", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--extensionTestsPath=${workspaceFolder}/test/suite/index" 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /test/suite/index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const Mocha = require('mocha'); 3 | const glob = require('glob'); 4 | 5 | function run() { 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((c, e) => { 15 | glob('**/**.test.js', { cwd: testsRoot }, (err, files) => { 16 | if (err) { 17 | return e(err); 18 | } 19 | 20 | // Add files to the test suite 21 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 22 | 23 | try { 24 | // Run the mocha test 25 | mocha.run(failures => { 26 | if (failures > 0) { 27 | e(new Error(`${failures} tests failed.`)); 28 | } else { 29 | c(); 30 | } 31 | }); 32 | } catch (err) { 33 | console.error(err); 34 | e(err); 35 | } 36 | }); 37 | }); 38 | } 39 | 40 | module.exports = { 41 | run 42 | }; 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2022 Peter Arato 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "byesig", 3 | "displayName": "byesig", 4 | "description": "Hide Ruby Sorbet signatures", 5 | "publisher": "itarato", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/itarato/byesig" 9 | }, 10 | "version": "0.0.16", 11 | "icon": "misc/logo.png", 12 | "engines": { 13 | "vscode": "^1.40.0" 14 | }, 15 | "categories": [ 16 | "Other" 17 | ], 18 | "activationEvents": [ 19 | "onLanguage:ruby" 20 | ], 21 | "main": "./extension.js", 22 | "contributes": { 23 | "commands": [ 24 | { 25 | "command": "byesig.hideSig", 26 | "title": "ByeSig: hide Sorbet signatures" 27 | }, 28 | { 29 | "command": "byesig.showSig", 30 | "title": "ByeSig: show Sorbet signatures" 31 | } 32 | ], 33 | "configuration": { 34 | "title": "Hide Ruby Sorbet signatures", 35 | "properties": { 36 | "byesig.enabled": { 37 | "type": "boolean", 38 | "description": "Enables/disables the default hiding mechanism", 39 | "default": true 40 | }, 41 | "byesig.fold": { 42 | "type": "boolean", 43 | "description": "Fold (collapse) sig blocks", 44 | "default": true 45 | }, 46 | "byesig.backgroundColor": { 47 | "type": "string", 48 | "description": "The background color of the hidden signature region (CSS notation, eg: #2266AA), leave empty for transparent", 49 | "default": "" 50 | }, 51 | "byesig.opacity": { 52 | "type": "number", 53 | "description": "Opacity of the signature area (0: transparent, 1: opaque)", 54 | "default": 0.1 55 | }, 56 | "byesig.showIcon": { 57 | "type": "boolean", 58 | "description": "Show a sig-line indicator icon on the left side gutter", 59 | "default": true 60 | } 61 | } 62 | } 63 | }, 64 | "scripts": { 65 | "lint": "eslint .", 66 | "pretest": "npm run lint", 67 | "test": "node ./test/runTest.js" 68 | }, 69 | "devDependencies": { 70 | "@types/vscode": "^1.40.0", 71 | "@types/glob": "^7.1.1", 72 | "@types/mocha": "^7.0.2", 73 | "@types/node": "^13.11.0", 74 | "eslint": "^6.8.0", 75 | "glob": "^7.1.6", 76 | "mocha": "^10.2.0", 77 | "typescript": "^3.8.3", 78 | "vscode-test": "^1.3.0", 79 | "lodash": ">=4.17.21" 80 | } 81 | } -------------------------------------------------------------------------------- /script/byesig.js: -------------------------------------------------------------------------------- 1 | const vscode = require('vscode'); 2 | const path = require('path'); 3 | 4 | const byesig = (function () { 5 | const COMMAND_FOLD = 'editor.fold'; 6 | const COMMAND_UNFOLD_ALL = 'editor.unfoldAll'; 7 | // @FIXME: Don't match with empty block. Empty block is not foldable by default and as such, folding 8 | // will affect the parent block - which we do not want. 9 | const RE_SIG_PREFIX = "^ *(T::Sig::WithoutRuntime\.)?sig(\\(:final\\))?"; 10 | const RE_SIG_BLOCK = `${RE_SIG_PREFIX} do$.*?^ *end\s*$`; 11 | const RE_SIG_LINE = `${RE_SIG_PREFIX} {.*?}.*?$`; 12 | 13 | const FORCE = true; 14 | 15 | let hideDelayTimeout; 16 | const DELAY_TIMEOUT_MS = 200; 17 | 18 | let byesigDecorationType = {}; 19 | let temporaryDisable = false; 20 | 21 | let knownDocuments = {}; 22 | 23 | function delayedHideSig() { 24 | if (hideDelayTimeout) clearTimeout(hideDelayTimeout); 25 | hideDelayTimeout = setTimeout(hideSig, DELAY_TIMEOUT_MS); 26 | } 27 | 28 | async function hideAndFoldSig(force = false) { 29 | hideSig(); 30 | await foldSig(force); 31 | } 32 | 33 | function decorationRenderOption() { 34 | let decoration = { 35 | opacity: vscode.workspace.getConfiguration('byesig').get('opacity').toString(), 36 | backgroundColor: vscode.workspace.getConfiguration('byesig').get('backgroundColor'), 37 | }; 38 | 39 | if (vscode.workspace.getConfiguration('byesig').get('showIcon')) { 40 | decoration['gutterIconPath'] = path.join(__dirname, '..', 'misc', 'icon.png'); 41 | decoration['gutterIconSize'] = "contain"; 42 | } 43 | 44 | return decoration; 45 | } 46 | 47 | function hideSig() { 48 | if (temporaryDisable) return; 49 | let editor = vscode.window.activeTextEditor; 50 | if (!editor) return; 51 | if (!isRubyFile(editor)) return; 52 | 53 | let tile_key = genTileKey(editor); 54 | disposeHidingDecoration(tile_key); 55 | byesigDecorationType[tile_key] = vscode.window.createTextEditorDecorationType(decorationRenderOption()); 56 | 57 | if (!vscode.workspace.getConfiguration('byesig').get('enabled')) return; 58 | 59 | editor.setDecorations(byesigDecorationType[tile_key], [ 60 | ...getMatchPositions(new RegExp(RE_SIG_BLOCK, "gsm"), editor), 61 | ...getMatchPositions(new RegExp(RE_SIG_LINE, "gsm"), editor) 62 | ]); 63 | } 64 | 65 | async function foldSig(force = false) { 66 | if (!vscode.workspace.getConfiguration('byesig').get('fold')) return; 67 | if (!vscode.workspace.getConfiguration('byesig').get('enabled')) return; 68 | 69 | if (temporaryDisable) return; 70 | 71 | let editor = vscode.window.activeTextEditor; 72 | if (!editor) return; 73 | 74 | if (!isRubyFile(editor)) return; 75 | if (!force && !isNewlyOpened(editor.document.fileName)) return; 76 | 77 | // When switching active editor, it takes time for VSCode to establish a selection. 78 | // This wait is so the old location of the cursor can be captured correctly. 79 | setTimeout(async () => { 80 | let original_selection = editor.selection; 81 | let folded_selections = getMatchPositions(new RegExp(RE_SIG_BLOCK, "gsm"), editor).map((range) => { 82 | let line_pos = editor.selection.active.with(range.start.line, 0); 83 | return new vscode.Selection(line_pos, line_pos); 84 | }); 85 | 86 | if (folded_selections.length > 0) { 87 | // This is a hack around not having an official API to find foldable regions (other than fold levels or all). 88 | editor.selections = folded_selections; 89 | await vscode.commands.executeCommand(COMMAND_UNFOLD_ALL); 90 | await vscode.commands.executeCommand(COMMAND_FOLD); 91 | 92 | // After selecting the fold regions (and fold) if we restore the cursor immediately sometimes VSCode makes a 93 | // selection with the delta. To avoid this, we wait a little. 94 | setTimeout(() => { 95 | editor.selections = [original_selection]; 96 | editor.revealRange(editor.selections[0]); 97 | }, 100); 98 | } 99 | }, 100); 100 | } 101 | 102 | /** 103 | * @param {RegExp} re 104 | * @param {import("vscode").TextEditor} editor 105 | */ 106 | function getMatchPositions(re, editor) { 107 | let match; 108 | let text = editor.document.getText(); 109 | let ranges = []; 110 | 111 | while ((match = re.exec(text))) { 112 | let start_pos = editor.document.positionAt(match.index); 113 | let end_pos = editor.document.positionAt(match.index + match[0].length); 114 | ranges.push(new vscode.Range(start_pos, end_pos)); 115 | } 116 | 117 | return ranges; 118 | } 119 | 120 | /** 121 | * @param {import("vscode").TextEditor} editor 122 | */ 123 | function isRubyFile(editor) { 124 | return editor.document.languageId == "ruby"; 125 | } 126 | 127 | async function showAndUnfoldSig() { 128 | disposeAllHidingDecoration(); 129 | if (!vscode.workspace.getConfiguration('byesig').get('enabled')) return; 130 | await vscode.commands.executeCommand(COMMAND_UNFOLD_ALL); 131 | } 132 | 133 | function disposeAllHidingDecoration() { 134 | for (let key in byesigDecorationType) { 135 | disposeHidingDecoration(key); 136 | } 137 | } 138 | 139 | /** 140 | * @param {string} key 141 | */ 142 | function disposeHidingDecoration(key) { 143 | if (!byesigDecorationType[key]) return; 144 | 145 | byesigDecorationType[key].dispose(); 146 | delete byesigDecorationType[key]; 147 | } 148 | 149 | function onCommandHideAndFoldSig() { 150 | temporaryDisable = false; 151 | hideAndFoldSig(FORCE); 152 | } 153 | 154 | function onCommandShowAndUnfoldSig() { 155 | temporaryDisable = true; 156 | showAndUnfoldSig(); 157 | } 158 | 159 | /** 160 | * @param {import("vscode").TextDocument} textDoc 161 | */ 162 | function onDidOpenTextDocument(textDoc) { 163 | if (textDoc.languageId != 'ruby') return 164 | if (knownDocuments[textDoc.fileName]) { 165 | knownDocuments[textDoc.fileName] += 1; 166 | } else { 167 | knownDocuments[textDoc.fileName] = 1; 168 | } 169 | } 170 | 171 | /** 172 | * @param {import("vscode").TextDocument} textDoc 173 | */ 174 | function onDidCloseTextDocument(textDoc) { 175 | if (textDoc.languageId != 'ruby') return 176 | if (!knownDocuments[textDoc.fileName]) return; 177 | 178 | delete knownDocuments[textDoc.fileName]; 179 | } 180 | 181 | /** 182 | * @param {string} fileName 183 | */ 184 | function isNewlyOpened(fileName) { 185 | return knownDocuments[fileName] == 1; 186 | } 187 | 188 | /** 189 | * @param {import("vscode").TextEditor} editor 190 | */ 191 | function genTileKey(editor) { 192 | return editor.document.fileName; 193 | } 194 | 195 | /** 196 | * @param {{ subscriptions: import("vscode").Disposable[]; }} context 197 | */ 198 | function activate(context) { 199 | context.subscriptions.push(vscode.commands.registerCommand('byesig.hideSig', onCommandHideAndFoldSig)); 200 | context.subscriptions.push(vscode.commands.registerCommand('byesig.showSig', onCommandShowAndUnfoldSig)); 201 | vscode.window.onDidChangeActiveTextEditor(() => { hideAndFoldSig(); }, null, context.subscriptions); 202 | vscode.workspace.onDidChangeTextDocument(delayedHideSig, null, context.subscriptions); 203 | vscode.workspace.onDidOpenTextDocument(onDidOpenTextDocument, null, context.subscriptions); 204 | vscode.workspace.onDidCloseTextDocument(onDidCloseTextDocument, null, context.subscriptions); 205 | 206 | if (vscode.window.activeTextEditor) hideAndFoldSig(); 207 | } 208 | 209 | return { activate: activate }; 210 | })(); 211 | 212 | module.exports = byesig; 213 | --------------------------------------------------------------------------------