├── .gitignore ├── logo.png ├── featureimages ├── newclass.gif ├── ctorfromprop.gif ├── fieldfromctor.gif ├── newinterface.gif ├── propfromctor.gif └── fullpropfromctor.gif ├── templates ├── class.tmpl └── interface.tmpl ├── .vscodeignore ├── tsconfig.json ├── .vscode ├── settings.json ├── launch.json └── tasks.json ├── test ├── extension.test.ts └── index.ts ├── licence.txt ├── README.md ├── CHANGELOG.md ├── vsc-extension-quickstart.md ├── package.json └── src ├── extension.ts └── codeActionProvider.ts /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | *.vsix -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchannon/csharpextensions/HEAD/logo.png -------------------------------------------------------------------------------- /featureimages/newclass.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchannon/csharpextensions/HEAD/featureimages/newclass.gif -------------------------------------------------------------------------------- /featureimages/ctorfromprop.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchannon/csharpextensions/HEAD/featureimages/ctorfromprop.gif -------------------------------------------------------------------------------- /featureimages/fieldfromctor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchannon/csharpextensions/HEAD/featureimages/fieldfromctor.gif -------------------------------------------------------------------------------- /featureimages/newinterface.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchannon/csharpextensions/HEAD/featureimages/newinterface.gif -------------------------------------------------------------------------------- /featureimages/propfromctor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchannon/csharpextensions/HEAD/featureimages/propfromctor.gif -------------------------------------------------------------------------------- /featureimages/fullpropfromctor.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jchannon/csharpextensions/HEAD/featureimages/fullpropfromctor.gif -------------------------------------------------------------------------------- /templates/class.tmpl: -------------------------------------------------------------------------------- 1 | namespace ${namespace} 2 | { 3 | public class ${classname} 4 | { 5 | ${cursor} 6 | } 7 | } -------------------------------------------------------------------------------- /templates/interface.tmpl: -------------------------------------------------------------------------------- 1 | namespace ${namespace} 2 | { 3 | public interface ${classname} 4 | { 5 | ${cursor} 6 | } 7 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | out/test/** 4 | test/** 5 | src/** 6 | **/*.map 7 | .gitignore 8 | tsconfig.json 9 | vsc-extension-quickstart.md 10 | *.vsix 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "rootDir": "." 11 | }, 12 | "exclude": [ 13 | "node_modules", 14 | ".vscode-test" 15 | ] 16 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | "typescript.tsdk": "./node_modules/typescript/lib" // we want to use the TS server from our node_modules folder to control its version 10 | } -------------------------------------------------------------------------------- /test/extension.test.ts: -------------------------------------------------------------------------------- 1 | // 2 | // Note: This example test is leveraging the Mocha test framework. 3 | // Please refer to their documentation on https://mochajs.org/ for help. 4 | // 5 | 6 | // The module 'assert' provides assertion methods from node 7 | import * as assert from 'assert'; 8 | 9 | // You can import and use all API from the 'vscode' module 10 | // as well as import your extension to test it 11 | import * as vscode from 'vscode'; 12 | import * as myExtension from '../src/extension'; 13 | 14 | // Defines a Mocha test suite to group tests of similar kind together 15 | suite("Extension Tests", () => { 16 | 17 | // Defines a Mocha unit test 18 | test("Something 1", () => { 19 | assert.equal(-1, [1, 2, 3].indexOf(5)); 20 | assert.equal(-1, [1, 2, 3].indexOf(0)); 21 | }); 22 | }); -------------------------------------------------------------------------------- /test/index.ts: -------------------------------------------------------------------------------- 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.ts (suite, test, etc.) 19 | useColors: true // colored output from test results 20 | }); 21 | 22 | module.exports = testRunner; -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it 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}" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": ["${workspaceRoot}/out/src/*"], 14 | "preLaunchTask": "npm" 15 | }, 16 | { 17 | "name": "Launch Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--extensionDevelopmentPath=${workspaceRoot}", "--extensionTestsPath=${workspaceRoot}/out/test" ], 22 | "stopOnEntry": false, 23 | "sourceMaps": true, 24 | "outFiles": ["${workspaceRoot}/out/test/*"], 25 | "preLaunchTask": "npm" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // Available variables which can be used inside of strings. 2 | // ${workspaceRoot}: the root folder of the team 3 | // ${file}: the current opened file 4 | // ${fileBasename}: the current opened file's basename 5 | // ${fileDirname}: the current opened file's dirname 6 | // ${fileExtname}: the current opened file's extension 7 | // ${cwd}: the current working directory of the spawned process 8 | 9 | // A task runner that calls a custom npm script that compiles the extension. 10 | { 11 | "version": "0.1.0", 12 | 13 | // we want to run npm 14 | "command": "npm", 15 | 16 | // the command is a shell script 17 | "isShellCommand": true, 18 | 19 | // show the output window only if unrecognized errors occur. 20 | "showOutput": "silent", 21 | 22 | // we run the custom script "compile" as defined in package.json 23 | "args": ["run", "compile", "--loglevel", "silent"], 24 | 25 | // The tsc compiler is started in watching mode 26 | "isWatching": true, 27 | 28 | // use the standard tsc in watch mode problem matcher to find compile problems in the output. 29 | "problemMatcher": "$tsc-watch" 30 | } -------------------------------------------------------------------------------- /licence.txt: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2016 Jonathan Channon and contributors 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ***** *PROJECT NO LONGER UNDER DEVELOPMENT* ***** 2 | 3 | # C# Extensions 4 | 5 | Welcome to C# Extensions. This VSCode extension provides extensions to the IDE that will hopefully speed up your development workflow. 6 | 7 | ## Features 8 | 9 | **Add C# Class** 10 | 11 | ![Add C# Class](./featureimages/newclass.gif) 12 | 13 | **Add C# Interface** 14 | 15 | ![Add C# Interface](./featureimages/newinterface.gif) 16 | 17 | **Add fields from constructors** 18 | 19 | ![Add fields from constructors](./featureimages/fieldfromctor.gif) 20 | 21 | **Add constructor from properties** 22 | 23 | ![Add constructor from properties](./featureimages/ctorfromprop.gif) 24 | 25 | **Add read-only property from constructors** 26 | ![Add read-only property from constructors](./featureimages/propfromctor.gif) 27 | 28 | **Add property from constructors** 29 | ![Add property from constructors](./featureimages/fullpropfromctor.gif) 30 | 31 | 32 | This extension traverses up the folder tree to find the project.json or *.csproj and uses that as the parent folder to determine namespaces. 33 | 34 | 35 | ----------------------------------------------------------------------------------------------------------- 36 | 37 | ## Licence 38 | 39 | MIT 40 | 41 | See [licence.txt](./licence.txt) 42 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [1.3.0] - 2017-03-01 5 | ### Added 6 | - Ability to create property and read-only property from constructor 7 | 8 | ### Changed 9 | - Fix for placing field outside class in some circumstances 10 | 11 | ## [1.2.1] - 2017-03-01 12 | ### Added 13 | - Ability to turn "this" prefix on and off 14 | 15 | ### Changed 16 | 17 | - Detect base classes for initializing members 18 | 19 | ## [1.2.0] - 2017-02-02 20 | ### Added 21 | - Ability to create new class/interface if using csproj files on .Net Core 22 | 23 | ## [1.1.0] - 2016-11-25 24 | ### Added 25 | - Initialize fields from constructor 26 | - Initialize constructor from properties 27 | 28 | ## [1.0.9] - 2016-11-03 29 | ### Changed 30 | - Bug fix for paths with spaces in them, creates namespace with underscore insteadnot sure 31 | 32 | ## [1.0.8] - 2016-10-28 33 | ### Changed 34 | - Bug fix for paths with multiple hyphens 35 | 36 | ## [1.0.7] - 2016-10-18 37 | ### Changed 38 | - Bug fix for extension of new file 39 | 40 | ## [1.0.6] - 2016-10-17 41 | ### Changed 42 | - If no extension exists then it will add .cs on the end 43 | 44 | ## [1.0.5] - 2016-10-17 45 | ### Changed 46 | - Ask only for filename instead of full path 47 | 48 | ## [1.0.4] - 2016-10-17 49 | ### Changed 50 | - If path containes hyphen in path, make sure this becomes an underscore like VS 51 | 52 | ## [1.0.3] - 2016-10-16 53 | ### Changed 54 | - Removed change log from release notes.md 55 | 56 | ## [1.0.2] - 2016-10-16 57 | ### Added 58 | - Works on Windows 59 | 60 | ## [1.0.1] - 2016-10-14 61 | ### Added 62 | - Can create class from root folder 63 | - Templates can specifiy where cursor exists 64 | 65 | ## [1.0.0] - 2016-10-14 66 | ### Added 67 | - Intial Release -------------------------------------------------------------------------------- /vsc-extension-quickstart.md: -------------------------------------------------------------------------------- 1 | # Welcome to your first VS Code Extension 2 | 3 | ## What's in the folder 4 | * This folder contains all of the files necessary for your extension 5 | * `package.json` - this is the manifest file in which you declare your extension and command. 6 | The sample plugin registers a command and defines its title and command name. With this information 7 | 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 10 | activated (in this case by executing the command). Inside the `activate` function we call `registerCommand`. 11 | We pass the function containing the implementation of the command as the second parameter to 12 | `registerCommand`. 13 | 14 | ## Get up and running straight away 15 | * press `F5` to open a new window with your extension loaded 16 | * run your command from the command palette by pressing (`Ctrl+Shift+P` or `Cmd+Shift+P` on Mac) and typing `Hello World` 17 | * set breakpoints in your code inside `src/extension.ts` to debug your extension 18 | * find output from your extension in the debug console 19 | 20 | ## Make changes 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 | ## Explore the API 25 | * you can open the full set of our API when you open the file `node_modules/vscode/vscode.d.ts` 26 | 27 | ## Run tests 28 | * open the debug viewlet (`Ctrl+Shift+D` or `Cmd+Shift+D` on Mac) and from the launch configuration dropdown pick `Launch Tests` 29 | * press `F5` to run the tests in a new window with your extension loaded 30 | * see the output of the test result in the debug console 31 | * make changes to `test/extension.test.ts` or create new test files inside the `test` folder 32 | * by convention, the test runner will only consider files matching the name pattern `**.test.ts` 33 | * you can create folders inside the `test` folder to structure your tests any way you want -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "csharpextensions", 3 | "displayName": "C# Extensions", 4 | "description": "C# IDE Extensions for VSCode", 5 | "author": "Jonathan Channon", 6 | "license": "SEE LICENSE IN license.txt", 7 | "version": "1.3.0", 8 | "publisher": "jchannon", 9 | "engines": { 10 | "vscode": "^1.5.0" 11 | }, 12 | "repository": "https://github.com/jchannon/csharpextensions", 13 | "icon": "logo.png", 14 | "categories": [ 15 | "Languages", 16 | "Linters", 17 | "Snippets" 18 | ], 19 | "activationEvents": [ 20 | "onLanguage:csharp", 21 | "onCommand:csharpextensions.createClass", 22 | "onCommand:csharpextensions.createInterface" 23 | ], 24 | "main": "./out/src/extension", 25 | "contributes": { 26 | "commands": [ 27 | { 28 | "command": "csharpextensions.createClass", 29 | "title": "New C# Class" 30 | }, 31 | { 32 | "command": "csharpextensions.createInterface", 33 | "title": "New C# Interface" 34 | } 35 | ], 36 | "menus": { 37 | "explorer/context": [ 38 | { 39 | "group": "navigation@-1", 40 | "command": "csharpextensions.createClass" 41 | }, 42 | { 43 | "group": "navigation@-1", 44 | "command": "csharpextensions.createInterface" 45 | } 46 | ] 47 | }, 48 | "configuration": { 49 | "title": "C# Extensions configuration", 50 | "properties": { 51 | "csharpextensions.privateMemberPrefix": { 52 | "type": "string", 53 | "default": "", 54 | "description": "Prefix for generated private member declarations" 55 | }, 56 | "csharpextensions.useThisForCtorAssignments": { 57 | "type": "boolean", 58 | "default": true, 59 | "description": "Wether or not a ctor assignment of a property or variable should be prefixed with this." 60 | }, 61 | "csharpextensions.reFormatAfterChange": { 62 | "type": "boolean", 63 | "default": true, 64 | "description": "If true the document will be reformatted after codeactions are used." 65 | } 66 | } 67 | } 68 | }, 69 | "scripts": { 70 | "vscode:prepublish": "node node_modules/typescript/bin/tsc -p ./", 71 | "compile": "node node_modules/typescript/bin/tsc -watch -p ./", 72 | "postinstall": "node ./node_modules/vscode/bin/install" 73 | }, 74 | "devDependencies": { 75 | "typescript": "^2.0.3", 76 | "vscode": "^1.0.0", 77 | "mocha": "^2.3.3", 78 | "@types/node": "^6.0.40", 79 | "@types/mocha": "^2.2.32" 80 | }, 81 | "dependencies": { 82 | "find-parent-dir": "^0.3.0", 83 | "find-up-glob": "^1.0.0" 84 | } 85 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | // The module 'vscode' contains the VS Code extensibility API 3 | // Import the module and reference it with the alias vscode in your code below 4 | import * as vscode from 'vscode'; 5 | import * as path from 'path'; 6 | import * as fs from 'fs'; 7 | import * as os from 'os'; 8 | import CodeActionProvider from './codeActionProvider'; 9 | const parentfinder = require('find-parent-dir'); 10 | const findupglob = require('find-up-glob'); 11 | 12 | // this method is called when your extension is activated 13 | // your extension is activated the very first time the command is executed 14 | export function activate(context: vscode.ExtensionContext) { 15 | const documentSelector: vscode.DocumentSelector = { 16 | language: 'csharp', 17 | scheme: 'file' 18 | }; 19 | // Use the console to output diagnostic information (console.log) and errors (console.error) 20 | // This line of code will only be executed once when your extension is activated 21 | //console.log('Congratulations, your extension "newclassextension" is now active!'); 22 | 23 | // The command has been defined in the package.json file 24 | // Now provide the implementation of the command with registerCommand 25 | // The commandId parameter must match the command field in package.json 26 | // let disposable = vscode.commands.registerCommand('extension.sayHello', (args) => { 27 | // // The code you place here will be executed every time your command is executed 28 | // // Display a message box to the user 29 | // vscode.window.showInformationMessage('Hello World!'); 30 | // }); 31 | 32 | //context.subscriptions.push(disposable); 33 | context.subscriptions.push(vscode.commands.registerCommand('csharpextensions.createClass', createClass)); 34 | context.subscriptions.push(vscode.commands.registerCommand('csharpextensions.createInterface', createInterface)); 35 | 36 | const codeActionProvider = new CodeActionProvider(); 37 | let disposable = vscode.languages.registerCodeActionsProvider(documentSelector, codeActionProvider); 38 | context.subscriptions.push(disposable); 39 | } 40 | 41 | function createClass(args) { 42 | promptAndSave(args, 'class'); 43 | } 44 | 45 | function createInterface(args) { 46 | promptAndSave(args, 'interface'); 47 | } 48 | 49 | function promptAndSave(args, templatetype: string) { 50 | if (args == null) { 51 | args = { _fsPath: vscode.workspace.rootPath } 52 | } 53 | let incomingpath: string = args._fsPath; 54 | vscode.window.showInputBox({ ignoreFocusOut: true, prompt: 'Please enter filename', value: 'new' + templatetype + '.cs' }) 55 | .then((newfilename) => { 56 | 57 | if (typeof newfilename === 'undefined') { 58 | return; 59 | } 60 | 61 | var newfilepath = incomingpath + path.sep + newfilename; 62 | 63 | if (fs.existsSync(newfilepath)) { 64 | vscode.window.showErrorMessage("File already exists"); 65 | return; 66 | } 67 | 68 | newfilepath = correctExtension(newfilepath); 69 | 70 | var originalfilepath = newfilepath; 71 | 72 | var projectrootdir = getProjectRootDirOfFilePath(newfilepath); 73 | 74 | if (projectrootdir == null) { 75 | vscode.window.showErrorMessage("Unable to find project.json or *.csproj"); 76 | return; 77 | } 78 | 79 | projectrootdir = removeTrailingSeparator(projectrootdir); 80 | 81 | var newroot = projectrootdir.substr(projectrootdir.lastIndexOf(path.sep) + 1); 82 | 83 | var filenamechildpath = newfilepath.substring(newfilepath.lastIndexOf(newroot)); 84 | 85 | var pathSepRegEx = /\//g; 86 | if (os.platform() === "win32") 87 | pathSepRegEx = /\\/g; 88 | 89 | var namespace = path.dirname(filenamechildpath); 90 | namespace = namespace.replace(pathSepRegEx, '.'); 91 | 92 | namespace = namespace.replace(/\s+/g, "_"); 93 | namespace = namespace.replace(/-/g, "_"); 94 | 95 | newfilepath = path.basename(newfilepath, '.cs'); 96 | 97 | openTemplateAndSaveNewFile(templatetype, namespace, newfilepath, originalfilepath); 98 | }); 99 | } 100 | 101 | function correctExtension(filename) { 102 | if (path.extname(filename) !== '.cs') { 103 | if (filename.endsWith('.')) { 104 | filename = filename + 'cs'; 105 | } else { 106 | filename = filename + '.cs'; 107 | } 108 | } 109 | return filename; 110 | } 111 | 112 | function removeTrailingSeparator(filepath) { 113 | if (filepath[filepath.length - 1] === path.sep) { 114 | filepath = filepath.substr(0, filepath.length - 1); 115 | } 116 | return filepath; 117 | } 118 | 119 | function getProjectRootDirOfFilePath(filepath) { 120 | var projectrootdir = parentfinder.sync(path.dirname(filepath), 'project.json'); 121 | if (projectrootdir == null) { 122 | var csprojfiles = findupglob.sync('*.csproj', { cwd: path.dirname(filepath) }); 123 | if (csprojfiles == null) { 124 | return null; 125 | } 126 | projectrootdir = path.dirname(csprojfiles[0]); 127 | } 128 | return projectrootdir; 129 | } 130 | 131 | function openTemplateAndSaveNewFile(type: string, namespace: string, filename: string, originalfilepath: string) { 132 | 133 | let templatefileName = type + '.tmpl'; 134 | 135 | vscode.workspace.openTextDocument(vscode.extensions.getExtension('jchannon.csharpextensions').extensionPath + '/templates/' + templatefileName) 136 | .then((doc: vscode.TextDocument) => { 137 | let text = doc.getText(); 138 | text = text.replace('${namespace}', namespace); 139 | text = text.replace('${classname}', filename); 140 | let cursorPosition = findCursorInTemlpate(text); 141 | text = text.replace('${cursor}', ''); 142 | fs.writeFileSync(originalfilepath, text); 143 | 144 | vscode.workspace.openTextDocument(originalfilepath).then((doc) => { 145 | vscode.window.showTextDocument(doc).then((editor) => { 146 | let newselection = new vscode.Selection(cursorPosition, cursorPosition); 147 | editor.selection = newselection; 148 | }); 149 | }); 150 | }); 151 | } 152 | 153 | function findCursorInTemlpate(text: string) { 154 | let cursorPos = text.indexOf('${cursor}'); 155 | let preCursor = text.substr(0, cursorPos); 156 | let lineNum = preCursor.match(/\n/gi).length; 157 | let charNum = preCursor.substr(preCursor.lastIndexOf('\n')).length; 158 | return new vscode.Position(lineNum, charNum); 159 | 160 | } 161 | 162 | // this method is called when your extension is deactivated 163 | export function deactivate() { 164 | } 165 | -------------------------------------------------------------------------------- /src/codeActionProvider.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | import * as vscode from 'vscode'; 3 | 4 | export default class CodeActionProvider implements vscode.CodeActionProvider{ 5 | private _commandIds = { 6 | ctorFromProperties: 'csharpextensions.ctorFromProperties', 7 | initializeMemberFromCtor: 'csharpextensions.initializeMemberFromCtor', 8 | }; 9 | 10 | constructor() { 11 | vscode.commands.registerCommand(this._commandIds.initializeMemberFromCtor, this.initializeMemberFromCtor, this); 12 | vscode.commands.registerCommand(this._commandIds.ctorFromProperties, this.executeCtorFromProperties, this); 13 | } 14 | 15 | 16 | public provideCodeActions(document:vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken) : vscode.Command[] { 17 | var commands = []; 18 | 19 | let addInitalizeFromCtor = (type:MemberGenerationType)=>{ 20 | var cmd = this.getInitializeFromCtorCommand(document,range,context,token, type); 21 | if(cmd) 22 | commands.push(cmd) 23 | }; 24 | 25 | addInitalizeFromCtor(MemberGenerationType.PrivateField); 26 | addInitalizeFromCtor(MemberGenerationType.ReadonlyProperty); 27 | addInitalizeFromCtor(MemberGenerationType.Property); 28 | 29 | var ctorPCommand = this.getCtorpCommand(document, range, context, token); 30 | if(ctorPCommand) 31 | commands.push(ctorPCommand); 32 | 33 | 34 | return commands; 35 | } 36 | 37 | private camelize(str) { 38 | return str.replace(/(?:^\w|[A-Z]|\b\w|\s+)/g, function(match, index) { 39 | if (+match === 0) return ""; // or if (/\s+/.test(match)) for white spaces 40 | return index == 0 ? match.toLowerCase() : match.toUpperCase(); 41 | }); 42 | } 43 | 44 | private executeCtorFromProperties(args:ConstructorFromPropertiesArgument) { 45 | var tabSize = vscode.workspace.getConfiguration().get('editor.tabSize', 4); 46 | let ctorParams = []; 47 | 48 | if(!args.properties) 49 | return; 50 | 51 | args.properties.forEach((p)=>{ 52 | ctorParams.push(`${p.type} ${this.camelize(p.name)}`) 53 | }); 54 | 55 | let assignments = []; 56 | args.properties.forEach((p)=>{ 57 | assignments.push(`${Array(tabSize*1).join(' ')} this.${p.name} = ${this.camelize(p.name)}; 58 | `); 59 | }); 60 | 61 | let firstPropertyLine = args.properties.sort((a,b)=>{ 62 | return a.lineNumber-b.lineNumber 63 | })[0].lineNumber; 64 | 65 | var ctorStatement = `${Array(tabSize*2).join(' ')} ${args.classDefinition.modifier} ${args.classDefinition.className}(${ctorParams.join(', ')}) 66 | { 67 | ${assignments.join('')} 68 | } 69 | `; 70 | 71 | let edit = new vscode.WorkspaceEdit(); 72 | let edits = []; 73 | 74 | let pos = new vscode.Position(firstPropertyLine, 0); 75 | let range = new vscode.Range(pos,pos); 76 | let ctorEdit = new vscode.TextEdit(range, ctorStatement); 77 | 78 | edits.push(ctorEdit) 79 | edit.set(args.document.uri, edits); 80 | 81 | let reFormatAfterChange = vscode.workspace.getConfiguration().get('csharpextensions.reFormatAfterChange', true); 82 | let applyPromise = vscode.workspace.applyEdit(edit) 83 | 84 | if(reFormatAfterChange){ 85 | applyPromise.then(()=>{ 86 | vscode.commands.executeCommand('vscode.executeFormatDocumentProvider', args.document.uri).then((formattingEdits:vscode.TextEdit[])=>{ 87 | var formatEdit = new vscode.WorkspaceEdit(); 88 | formatEdit.set(args.document.uri,formattingEdits); 89 | vscode.workspace.applyEdit(formatEdit); 90 | }); 91 | }) 92 | } 93 | } 94 | 95 | private getCtorpCommand(document:vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken) : vscode.Command { 96 | const editor = vscode.window.activeTextEditor; 97 | const position = editor.selection.active; 98 | 99 | let withinClass = this.findClassFromLine(document, position.line); 100 | if(!withinClass) 101 | return null; 102 | 103 | let properties = []; 104 | let lineNo = 0; 105 | 106 | while(lineNo < document.lineCount) { 107 | let readonlyRegex = new RegExp(/(public|private|protected)\s(\w+)\s(\w+)\s?{\s?(get;)\s?(private\s)?(set;)?\s?}/g); 108 | let textLine = document.lineAt(lineNo); 109 | let match = readonlyRegex.exec(textLine.text); 110 | 111 | if(match){ 112 | let clazz = this.findClassFromLine(document,lineNo); 113 | if(clazz.className === withinClass.className) { 114 | let prop: CSharpPropertyDefinition = { 115 | lineNumber: lineNo, 116 | class: clazz, 117 | modifier: match[1], 118 | type: match[2], 119 | name: match[3], 120 | statement: match[0] 121 | } 122 | 123 | properties.push(prop); 124 | } 125 | } 126 | lineNo+=1; 127 | } 128 | 129 | if(!properties.length) 130 | return null; 131 | 132 | var classDefinition = this.findClassFromLine(document, position.line); 133 | if(!classDefinition) 134 | return; 135 | 136 | var parameter : ConstructorFromPropertiesArgument = { 137 | properties: properties, 138 | classDefinition: classDefinition, 139 | document: document 140 | }; 141 | 142 | let cmd: vscode.Command = { 143 | title: "Initialize ctor from properties...", 144 | command: this._commandIds.ctorFromProperties, 145 | arguments: [parameter] 146 | }; 147 | 148 | return cmd; 149 | } 150 | 151 | private findClassFromLine(document:vscode.TextDocument, lineNo:number) : CSharpClassDefinition { 152 | var classRegex = new RegExp(/(private|internal|public|protected)\s?(static)?\sclass\s(\w*)/g); 153 | while(lineNo > 0){ 154 | var line = document.lineAt(lineNo); 155 | let match; 156 | if((match = classRegex.exec(line.text))){ 157 | return { 158 | startLine: lineNo, 159 | endLine: -1, 160 | className: match[3], 161 | modifier: match[1], 162 | statement: match[0] 163 | }; 164 | } 165 | lineNo -=1; 166 | } 167 | return null; 168 | } 169 | 170 | private initializeMemberFromCtor(args:InitializeFieldFromConstructor) { 171 | let edit = new vscode.WorkspaceEdit(); 172 | 173 | var bodyStartRange = new vscode.Range(args.constructorBodyStart, args.constructorBodyStart) 174 | var declarationRange = new vscode.Range(args.constructorStart, args.constructorStart); 175 | 176 | let declarationEdit = new vscode.TextEdit(declarationRange, args.memberGeneration.declaration); 177 | let memberInitEdit = new vscode.TextEdit(bodyStartRange, args.memberGeneration.assignment); 178 | 179 | var edits = []; 180 | if(args.document.getText().indexOf(args.memberGeneration.declaration.trim()) == -1){ 181 | edits.push(declarationEdit); 182 | } 183 | 184 | if(args.document.getText().indexOf(args.memberGeneration.assignment.trim())== -1){ 185 | edits.push(memberInitEdit); 186 | } 187 | 188 | edit.set(args.document.uri, edits); 189 | 190 | var reFormatAfterChange = vscode.workspace.getConfiguration().get('csharpextensions.reFormatAfterChange', true); 191 | var applyPromise = vscode.workspace.applyEdit(edit); 192 | 193 | if(reFormatAfterChange){ 194 | applyPromise.then(()=>{ 195 | vscode.commands.executeCommand('vscode.executeFormatDocumentProvider', args.document.uri).then((formattingEdits:vscode.TextEdit[])=>{ 196 | var formatEdit = new vscode.WorkspaceEdit(); 197 | formatEdit.set(args.document.uri,formattingEdits); 198 | vscode.workspace.applyEdit(formatEdit); 199 | }); 200 | }) 201 | } 202 | } 203 | 204 | private getInitializeFromCtorCommand(document:vscode.TextDocument, range: vscode.Range, context: vscode.CodeActionContext, token: vscode.CancellationToken, memberGenerationType: MemberGenerationType ):vscode.Command { 205 | const editor = vscode.window.activeTextEditor; 206 | const position = editor.selection.active; 207 | var surrounding = document.getText(new vscode.Range(new vscode.Position(position.line-2,0),new vscode.Position(position.line+2,0))); 208 | let wordRange = editor.document.getWordRangeAtPosition(position); 209 | if(!wordRange) 210 | return null; 211 | 212 | var regex = new RegExp(/(public|private|protected)\s(.*?)\(([\s\S]*?)\)/gi); 213 | var matches = regex.exec(surrounding); 214 | if(!matches) 215 | return null; 216 | 217 | var ctorStartPos = editor.document.getText().indexOf(matches[0]); 218 | 219 | var ctorParamStr = matches[3]; 220 | let parameters = {}; 221 | ctorParamStr.split(',').forEach((match)=>{ 222 | var separated = match.trim().split(' '); 223 | var type = separated[0].trim(); 224 | var name = separated[1].trim(); 225 | parameters[name] = type; 226 | }); 227 | 228 | var lineText = editor.document.getText(new vscode.Range(position.line,0,position.line,wordRange.end.character)); 229 | var selectedName = lineText.substr(wordRange.start.character,wordRange.end.character-wordRange.start.character); 230 | var parameterType = parameters[selectedName]; 231 | if(!parameterType){ 232 | return; 233 | } 234 | 235 | var tabSize = vscode.workspace.getConfiguration().get('editor.tabSize', 4); 236 | var privateMemberPrefix = vscode.workspace.getConfiguration().get('csharpextensions.privateMemberPrefix', ''); 237 | var prefixWithThis = vscode.workspace.getConfiguration().get('csharpextensions.useThisForCtorAssignments', true); 238 | 239 | let memberGeneration:MemberGenerationProperties = null; 240 | let title = ""; 241 | if(memberGenerationType === MemberGenerationType.PrivateField){ 242 | title = "Initialize field from parameter..." 243 | memberGeneration = { 244 | type: memberGenerationType, 245 | declaration: `${Array(tabSize*2).join(' ')} private readonly ${parameterType} ${privateMemberPrefix}${selectedName};\r\n`, 246 | assignment: `${Array(tabSize*3).join(' ')} ${(prefixWithThis?'this.':'')}${privateMemberPrefix}${selectedName} = ${selectedName};\r\n` 247 | }; 248 | } else if( memberGenerationType === MemberGenerationType.ReadonlyProperty){ 249 | title = "Initialize readonly property from parameter..."; 250 | var name = selectedName[0].toUpperCase()+selectedName.substr(1); 251 | memberGeneration = { 252 | type: memberGenerationType, 253 | declaration: `${Array(tabSize*2).join(' ')} public ${parameterType} ${name} { get; }\r\n`, 254 | assignment: `${Array(tabSize*3).join(' ')} ${(prefixWithThis?'this.':'')}${name} = ${selectedName};\r\n` 255 | }; 256 | } else if( memberGenerationType === MemberGenerationType.Property){ 257 | title = "Initialize property from parameter..."; 258 | var name = selectedName[0].toUpperCase()+selectedName.substr(1); 259 | memberGeneration = { 260 | type: memberGenerationType, 261 | declaration: `${Array(tabSize*2).join(' ')} public ${parameterType} ${name} { get;set; }\r\n`, 262 | assignment: `${Array(tabSize*3).join(' ')} ${(prefixWithThis?'this.':'')}${name} = ${selectedName};\r\n` 263 | }; 264 | } 265 | 266 | var parameter:InitializeFieldFromConstructor = { 267 | document: document, 268 | type: parameterType, 269 | name: selectedName, 270 | memberGeneration: memberGeneration, 271 | constructorBodyStart: this.findConstructorBodyStart(document,position), 272 | constructorStart: this.findConstructorStart(document,position) 273 | }; 274 | 275 | let cmd: vscode.Command = { 276 | title: title, 277 | command: this._commandIds.initializeMemberFromCtor, 278 | arguments: [parameter] 279 | }; 280 | 281 | return cmd; 282 | } 283 | 284 | private findConstructorBodyStart(document:vscode.TextDocument, position:vscode.Position): vscode.Position { 285 | for (var lineNo = position.line; lineNo < position.line+5; lineNo++) { 286 | var line = document.lineAt(lineNo); 287 | var braceIdx = line.text.indexOf('{'); 288 | if(braceIdx != -1){ 289 | return new vscode.Position(lineNo+1,0); 290 | } 291 | } 292 | return null; 293 | } 294 | 295 | private findConstructorStart(document:vscode.TextDocument, position:vscode.Position): vscode.Position { 296 | var clazz = this.findClassFromLine(document, position.line); 297 | 298 | for (var lineNo = position.line; lineNo > position.line-5; lineNo--) { 299 | var line = document.lineAt(lineNo); 300 | if(line.isEmptyOrWhitespace && !(line.lineNumber < clazz.startLine)) { 301 | return new vscode.Position(lineNo, 0); 302 | } 303 | } 304 | 305 | return new vscode.Position(position.line, 0); 306 | } 307 | } 308 | 309 | enum MemberGenerationType { 310 | Property, 311 | ReadonlyProperty, 312 | PrivateField 313 | } 314 | 315 | interface MemberGenerationProperties { 316 | type: MemberGenerationType, 317 | assignment: string, 318 | declaration: string 319 | } 320 | 321 | interface CSharpClassDefinition { 322 | startLine: number, 323 | endLine: number, 324 | className: string, 325 | modifier: string, 326 | statement: string 327 | } 328 | 329 | interface CSharpPropertyDefinition { 330 | class: CSharpClassDefinition, 331 | modifier: string, 332 | type: string, 333 | name: string, 334 | statement: string, 335 | lineNumber: number 336 | } 337 | 338 | interface ConstructorFromPropertiesArgument { 339 | document: vscode.TextDocument, 340 | classDefinition: CSharpClassDefinition, 341 | properties: CSharpPropertyDefinition[] 342 | } 343 | 344 | interface InitializeFieldFromConstructor { 345 | document: vscode.TextDocument, 346 | type:string, 347 | name: string, 348 | memberGeneration: MemberGenerationProperties, 349 | constructorBodyStart: vscode.Position, 350 | constructorStart: vscode.Position, 351 | } --------------------------------------------------------------------------------