├── .eslintrc.json ├── .gitignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── images ├── Example.gif └── logo.png ├── jsconfig.json ├── package.json ├── src └── extension.js ├── test └── index.js └── workspace └── example.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": true, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "warn", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "warn", 20 | "constructor-super": "warn", 21 | "valid-typeof": "warn" 22 | } 23 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | *.vsix 3 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | { 3 | "version": "0.1.0", 4 | "configurations": [ 5 | { 6 | "name": "Launch Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "${workspaceRoot}/workspace" ], 11 | "stopOnEntry": false 12 | }, 13 | { 14 | "name": "Launch Tests", 15 | "type": "extensionHost", 16 | "request": "launch", 17 | "runtimeExecutable": "${execPath}", 18 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/test" ], 19 | "stopOnEntry": false 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | jsconfig.json 6 | vsc-extension-quickstart.md 7 | .eslintrc.json 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to the "multi-cursor-case-preserve" extension will be documented in this file. 4 | 5 | Check [Keep a Changelog](http://keepachangelog.com/) for recommendations on how to structure this file. 6 | 7 | ## [1.0.5] - 2020-01-22 8 | 9 | * Fix a rare exception 10 | 11 | ## [1.0.4] - 2018-05-08 12 | 13 | * More work on undo/redo 14 | 15 | ## [1.0.3] - 2018-05-06 16 | 17 | * Improves undo/redo behavior 18 | 19 | ## [1.0.2] - 2017-06-26 20 | 21 | * Fixes bugs introduced in 1.0.1 22 | 23 | ## [1.0.1] - 2017-06-22 24 | 25 | * Now it doesn't changing anything, if case was equal in the first place - prevents some annoying false positives. 26 | 27 | ## [1.0.0] - 2017-06-16 28 | 29 | * Initial release 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Cardinal90 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Multiple cursor case preserve 2 | 3 | ## Features 4 | 5 | Have you ever tried to change a single word in all variable names, but had your camelCase broken? This extension preserves selection case in these situations. It recognises CAPS, Uppercase and lowercase. Works for typing or pasting. 6 | 7 |  8 | 9 | ## Known Issues 10 | 11 | * History for redo breaks when you undo a change made by the extension after pasting into multiple selections. [This issue](https://github.com/Microsoft/vscode/issues/38535) prevents me from solving undo/redo completely. 12 | * Undo/redo works one change at a time, which for CAPS usually means a **ctrl+z** for every symbol. 13 | 14 | ## Release Notes 15 | 16 | ### 1.0.5 17 | 18 | Fix a rare exception 19 | 20 | ### 1.0.4 21 | 22 | More work on undo/redo 23 | 24 | ### 1.0.3 25 | 26 | Improves undo/redo behavior 27 | 28 | ### 1.0.2 29 | 30 | Fixes bugs introduced in 1.0.1 31 | 32 | ### 1.0.1 33 | 34 | Now it doesn't changing anything, if case was equal in the first place - prevents some annoying false positives. 35 | 36 | ### 1.0.0 37 | 38 | Initial release 39 | -------------------------------------------------------------------------------- /images/Example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cardinal90/multi-cursor-case-preserve/5a18cf8b1f82b64e8b36e225e8009972692e3e93/images/Example.gif -------------------------------------------------------------------------------- /images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Cardinal90/multi-cursor-case-preserve/5a18cf8b1f82b64e8b36e225e8009972692e3e93/images/logo.png -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "lib": [ 6 | "es6" 7 | ] 8 | }, 9 | "exclude": [ 10 | "node_modules" 11 | ] 12 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "multi-cursor-case-preserve", 3 | "displayName": "Multiple cursor case preserve", 4 | "description": "Preserves case when editing with multiple cursors", 5 | "icon": "images/logo.png", 6 | "bugs": { 7 | "url": "https://github.com/Cardinal90/multi-cursor-case-preserve/issues" 8 | }, 9 | "homepage": "https://github.com/Cardinal90/multi-cursor-case-preserve/blob/master/README.md", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/Cardinal90/multi-cursor-case-preserve.git" 13 | }, 14 | "license": "MIT", 15 | "version": "1.0.5", 16 | "publisher": "Cardinal90", 17 | "engines": { 18 | "vscode": "^1.0.0" 19 | }, 20 | "categories": [ 21 | "Other" 22 | ], 23 | "keywords": [ 24 | "case", 25 | "multiple cursors", 26 | "multi-cursor", 27 | "camelCase", 28 | "preserve case" 29 | ], 30 | "activationEvents": [ 31 | "*" 32 | ], 33 | "main": "./src/extension", 34 | "scripts": { 35 | "postinstall": "node ./node_modules/vscode/bin/install", 36 | "test": "node ./node_modules/vscode/bin/test" 37 | }, 38 | "devDependencies": { 39 | "typescript": "^2.0.3", 40 | "vscode": "^1.0.0", 41 | "mocha": "^2.3.3", 42 | "eslint": "^3.6.0", 43 | "@types/node": "^6.0.40", 44 | "@types/mocha": "^2.2.32" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/extension.js: -------------------------------------------------------------------------------- 1 | var vscode = require('vscode'); 2 | 3 | var window = vscode.window; 4 | var Disposable = vscode.Disposable; 5 | var Range = vscode.Range; 6 | 7 | class MultiCursorCasePreserve { 8 | constructor() { 9 | this.store = new Map(); 10 | } 11 | 12 | createLineArray(args) { 13 | return args.selections.map(function(selection) { 14 | return args.textEditor.document.lineAt(selection.start.line); 15 | }); 16 | } 17 | 18 | createNewEditorState(args) { 19 | var state = { 20 | selectionsData: [], 21 | numberOfSelections: args.selections.length, 22 | lines: this.createLineArray(args), 23 | }; 24 | this.store.set(args.textEditor, state); 25 | return state; 26 | } 27 | 28 | areSelectionsEmpty(args) { 29 | return args.selections.every(function(selection) { 30 | return selection.isEmpty; 31 | }); 32 | } 33 | 34 | areSelectionsEqualOrEmpty(args) { 35 | var text = ''; 36 | return args.selections.every(function(selection) { 37 | text = !text && !selection.isEmpty ? args.textEditor.document.getText(selection).toLowerCase() : text; 38 | return selection.isEmpty || args.textEditor.document.getText(selection).toLowerCase() === text; 39 | }); 40 | } 41 | 42 | areSelectionsStrictlyEqualOrEmpty(args) { 43 | var text = ''; 44 | return args.selections.every(function(selection) { 45 | text = !text && !selection.isEmpty ? args.textEditor.document.getText(selection) : text; 46 | return selection.isEmpty || args.textEditor.document.getText(selection) === text; 47 | }); 48 | } 49 | 50 | areRangesEqualLength(selectionsData) { 51 | var firstLen = selectionsData[0].range.end.character - selectionsData[0].range.start.character; 52 | 53 | for (let i = 0; i < selectionsData.length; i++) { 54 | if (selectionsData[i].range.end.line !== selectionsData[i].range.start.line) { 55 | return false; 56 | } 57 | var len = selectionsData[i].range.end.character - selectionsData[i].range.start.character; 58 | if (len !== firstLen) { 59 | return false; 60 | } 61 | } 62 | return true; 63 | } 64 | 65 | initSelectionsData(args, state) { 66 | return args.selections.reduce(function(selectionsData, selection, index) { 67 | var text = args.textEditor.document.getText(selection); 68 | selectionsData[index] = selectionsData[index] || {}; 69 | selectionsData[index].text = text; 70 | selectionsData[index].start = selection.start; 71 | return selectionsData; 72 | }, state.selectionsData); 73 | } 74 | 75 | categorizeSelections(state) { 76 | state.selectionsData.forEach(function(selectionData) { 77 | if (/^[^a-z]+$/.test(selectionData.text) && selectionData.text.length > 1) { 78 | selectionData.type = 'caps'; 79 | } else if (/^[a-z].*/.test(selectionData.text)) { 80 | selectionData.type = 'lower'; 81 | } else if (/^[A-Z].*/.test(selectionData.text)) { 82 | selectionData.type = 'upper'; 83 | } 84 | }); 85 | } 86 | 87 | calculateSelectionRanges(args, state) { 88 | var count = 0; 89 | var len = 1; 90 | var line = -1; 91 | return args.selections.reduce(function(selectionsData, selection, index) { 92 | selectionsData[index] = selectionsData[index] || {}; 93 | if (selectionsData[index].start.line === line) { 94 | count++; 95 | } else { 96 | count = 0; 97 | len = selection.end.character - selectionsData[index].start.character; 98 | } 99 | line = selectionsData[index].start.line; 100 | selectionsData[index].range = new Range( 101 | selectionsData[index].start.translate(0, count * (len - selectionsData[index].text.length)), 102 | selection.end 103 | ); 104 | return selectionsData; 105 | }, state.selectionsData); 106 | } 107 | 108 | editSelections(args, state) { 109 | if ( 110 | state.selectionsData.every(function(data) { 111 | return data.text.length === 0; 112 | }) 113 | ) { 114 | return; 115 | } 116 | 117 | var edits = []; 118 | 119 | state.selectionsData.forEach(function(selectionData) { 120 | var text = args.textEditor.document.getText(selectionData.range); 121 | var newText = text; 122 | switch (selectionData.type) { 123 | case 'caps': 124 | newText = text.toUpperCase(); 125 | break; 126 | case 'lower': 127 | newText = text[0].toLowerCase() + text.substring(1); 128 | break; 129 | case 'upper': 130 | newText = text[0].toUpperCase() + text.substring(1); 131 | break; 132 | } 133 | if (text !== newText) { 134 | edits.push({ 135 | range: selectionData.range, 136 | text: newText, 137 | }); 138 | } 139 | }); 140 | 141 | if (edits.length === 0) { 142 | return; 143 | } 144 | 145 | var self = this; 146 | 147 | args.textEditor 148 | .edit( 149 | function(textEditorEdit) { 150 | edits.forEach(function(edit) { 151 | textEditorEdit.replace(edit.range, edit.text); 152 | }); 153 | }, { 154 | undoStopAfter: false, 155 | undoStopBefore: false, 156 | } 157 | ) 158 | .then( 159 | function(result) { 160 | if (result === true) { 161 | state.lines = self.createLineArray(args); 162 | } 163 | }, 164 | function(err) {} 165 | ); 166 | } 167 | 168 | update(args) { 169 | if (!args || !args.selections || !args.textEditor) { 170 | return; 171 | } 172 | 173 | // Work only with two or more selections... 174 | if (args.selections.length < 2) { 175 | this.store.delete(args.textEditor); 176 | return; 177 | } 178 | 179 | //, which are either equal or empty, but not strictly equal 180 | if (!this.areSelectionsEqualOrEmpty(args) || 181 | (this.areSelectionsStrictlyEqualOrEmpty(args) && !this.areSelectionsEmpty(args))) { 182 | return; 183 | } 184 | 185 | // Sorting is needed to correctly process multiple selections in the same line 186 | args.selections.sort(function(a, b) { 187 | return a.start.compareTo(b.start); 188 | }); 189 | 190 | // For every text editor there is a separate current state 191 | var state = this.store.get(args.textEditor) || this.createNewEditorState(args); 192 | 193 | // If number of selections is different, it is a clear marker to recalculate everything 194 | if (args.selections.length !== state.numberOfSelections) { 195 | state = this.createNewEditorState(args); 196 | } 197 | 198 | // Check if all selections are empty 199 | var selectionIsEmpty = this.areSelectionsEmpty(args); 200 | 201 | // If no lines changed, it is a selection stage 202 | // !selectionIsEmpty check is needed because after undo lines will be changed and selected 203 | if (!selectionIsEmpty) { 204 | state.selectionsData = this.initSelectionsData(args, state); 205 | this.categorizeSelections(state); 206 | state.lines = this.createLineArray(args); 207 | 208 | // If something changed, user just typed something, so we need to recalculate ranges for replacements 209 | // (recalculation on every step is needed to correctly handle multiple selections in the same line) 210 | } else { 211 | state.selectionsData = this.calculateSelectionRanges(args, state); 212 | var rangesAreEqual = this.areRangesEqualLength(state.selectionsData); 213 | if (rangesAreEqual) { 214 | this.editSelections(args, state); 215 | } 216 | } 217 | } 218 | 219 | dispose() {} 220 | } 221 | 222 | class multiCursorCasePreserveController { 223 | constructor(multiCursorCasePreserve) { 224 | this._multiCursorCasePreserve = multiCursorCasePreserve; 225 | 226 | let subscriptions = []; 227 | window.onDidChangeTextEditorSelection(this._onEvent, this, subscriptions); 228 | window.onDidChangeActiveTextEditor(this._onEvent, this, subscriptions); 229 | 230 | this._disposable = Disposable.from(...subscriptions); 231 | } 232 | 233 | dispose() { 234 | this._disposable.dispose(); 235 | } 236 | 237 | _onEvent(args) { 238 | this._multiCursorCasePreserve.update(args); 239 | } 240 | } 241 | 242 | function activate(context) { 243 | // console.log('activated'); 244 | let multiCursorCasePreserve = new MultiCursorCasePreserve(); 245 | let controller = new multiCursorCasePreserveController(multiCursorCasePreserve); 246 | 247 | context.subscriptions.push(controller); 248 | context.subscriptions.push(multiCursorCasePreserve); 249 | } 250 | exports.activate = activate; 251 | 252 | function deactivate() {} 253 | exports.deactivate = deactivate; 254 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // PLEASE DO NOT MODIFY / DELETE UNLESS YOU KNOW WHAT YOU ARE DOING 3 | // 4 | // This file is providing the test runner to use when running extension tests. 5 | // By default the test runner in use is Mocha based. 6 | // 7 | // You can provide your own test runner if you want to override it by exporting 8 | // a function run(testRoot: string, clb: (error:Error) => void) that the extension 9 | // host can call to run the tests. The test runner is expected to use console.log 10 | // to report the results back to the caller. When the tests are finished, return 11 | // a possible error to the callback or null if none. 12 | 13 | var testRunner = require('vscode/lib/testrunner'); 14 | 15 | // You can directly control Mocha options by uncommenting the following lines 16 | // See https://github.com/mochajs/mocha/wiki/Using-mocha-programmatically#set-options for more info 17 | testRunner.configure({ 18 | ui: 'tdd', // the TDD UI is being used in extension.test.js (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /workspace/example.js: -------------------------------------------------------------------------------- 1 | var element1, element2, a, b, ElementController; 2 | 3 | const ELEMENT_NUMBER = 42; 4 | 5 | var element = 3; 6 | 7 | const ELEMENT_COUNT = 42; 8 | 9 | var $element, listOfElements; 10 | 11 | var currentElement = new ElementController(); 12 | 13 | document.body.innerHTML = '
'; 14 | 15 | Foo, foo, FOO, foo; 16 | --------------------------------------------------------------------------------