├── .gitignore ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── images ├── 1.gif ├── 2.gif ├── 3.gif ├── 4.gif └── icon.png ├── package.json ├── src ├── extension.ts └── formatter.ts ├── test ├── extension.test.ts ├── index.ts └── testcase.txt ├── tsconfig.json └── vsc-extension-quickstart.md /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules -------------------------------------------------------------------------------- /.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/**/*.js" ], 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/**/*.js" ], 25 | "preLaunchTask": "npm" 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.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 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | .git/** 4 | out/test/** 5 | test/** 6 | src/** 7 | **/*.map 8 | **/*.ts 9 | .gitignore 10 | tsconfig.json 11 | vsc-extension-quickstart.md 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 1.1.7 2 | ===== 3 | - Fix #8, '==' is interpreted as two '='. 4 | 5 | 1.1.6 6 | ===== 7 | - Enhance the logic of finding which op to be aligned. 8 | 9 | 1.1.5 10 | ===== 11 | 12 | ## Bug 13 | - Fix plugin not working when language config is present in user settings, but it doesn't include better-align settings. 14 | 15 | 16 | 1.1.4 17 | ===== 18 | 19 | ## Bug 20 | - Fix #5, `+=` was changed to `=` 21 | - Fix #4, double slash after colon is being recognized as comment. e.g. `http://` 22 | 23 | 24 | 1.1.3 25 | ===== 26 | 27 | ## What's New 28 | - Always align with assignment operator if it's presented. 29 | - User can stop better-align from modifying the indentation ( specified in settings : `alignment.indentBase` ) 30 | - Allow space to the sibling token ( check out README.md to know how to enable this mode ): 31 | 32 | ``` 33 | // Before 34 | export fdafas=fdasfas; 35 | export fs=fasfdsfadsa; 36 | export fadsfasf=fadsjfkdasf; 37 | export fadsfa=fadfdasfadsf; 38 | 39 | // After 40 | export fdafas=fdasfas; 41 | export fs=fasfdsfadsa; 42 | export fadsfasf=fadsjfkdasf; 43 | export fadsfa=fadfdasfadsf; 44 | ``` 45 | 46 | 1.1.2 47 | ===== 48 | 49 | ## Bug 50 | - Don't align '->', because it's commonly used as attribute access operator. 51 | 52 | 1.1.1 53 | ===== 54 | 55 | ## What's New 56 | - Arrows (-> =>) can be aligned 57 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | opyright (c) 2010-2016 James Hall, https://github.com/MrRio/jsPDF 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Better Align 2 | 3 | [![Current Version](http://vsmarketplacebadge.apphb.com/version-short/wwm.better-align.svg)](https://marketplace.visualstudio.com/items?itemName=wwm.better-align) 4 | [![Install Count](http://vsmarketplacebadge.apphb.com/installs-short/wwm.better-align.svg)](https://marketplace.visualstudio.com/items?itemName=wwm.better-align) 5 | [![Rating](http://vsmarketplacebadge.apphb.com/rating-short/wwm.better-align.svg)](https://marketplace.visualstudio.com/items?itemName=wwm.better-align) 6 | 7 | ## Features 8 | 9 | Align your code by colon(`:`), assignment(`=`,`+=`,`-=`,`*=`,`/=`) and arrow(`=>`). 10 | It has additional support for comma-first coding style and trailing comment. 11 | 12 | And it doesn't require you to select what to be aligned, the extension will figure it out by itself. 13 | 14 | ## How to use 15 | 16 | Place your cursor at where you want your code to be aligned, and invoke the Align command via Command Palette or customized shortcut. Then the code will be automatically aligned. 17 | 18 | ## Screenshots 19 | 20 | Comma-first sytle: 21 | 22 | ![Comma-first style](images/1.gif) 23 | 24 | Trailing comment: 25 | 26 | ![Trailing comment](images/2.gif) 27 | 28 | Align within selection: 29 | 30 | ![Select a wide range and align them all](images/3.gif) 31 | 32 | ## Shortcuts 33 | 34 | There's no built-in shortcut comes with the extension, you have to add shotcuts by yourself: 35 | 1. Open Command Palette and type `open shortcuts` to open keybinding settings 36 | 2. Add something similar like this: 37 | ``` 38 | { "key": "ctrl+cmd+=", "command": "wwm.aligncode", 39 | "when": "editorTextFocus && !editorReadonly" } 40 | ``` 41 | 42 | ## Extension Settings 43 | 44 | ### `alignment.operatorPadding` : "left" | "right" 45 | 46 | Specify how assignment operator will be aligned. 47 | ``` 48 | // Original code 49 | this.abc=10; 50 | this.cd+=12; 51 | 52 | // left 53 | this.abc = 10; 54 | this.cd += 12; 55 | 56 | // right 57 | this.abc = 10; 58 | this.cd += 12; 59 | ``` 60 | 61 | ### `alignment.indentBase` : "firstline" | "activeline" | "dontchange" 62 | Specify if it use the indentation of the firstline or the line under the cursor. Below are the `activeline` effect, notice how it's different from the screenshot above. 63 | 64 | If `indentBase` is `dontchange`, better-align will only align lines with same indentation and will not modify the indentation. 65 | 66 | ![activeline effect](images/4.gif) 67 | 68 | ### `alignment.surroundSpace` 69 | Default value: 70 | ``` 71 | alignment.surroundSpace : { 72 | "colon" : [0, 1], // The first number specify how much space to add to the left, can be negative. 73 | // The second number is how much space to the right, can be negative. 74 | "assignment" : [1, 1], // The same as above. 75 | "arrow" : [1, 1], // The same as above. 76 | "comment" : 2 // Special how much space to add between the trailing comment and the code. 77 | // If this value is negative, it means don't align the trailing comment. 78 | } 79 | ``` 80 | 81 | ``` 82 | // Orignal code 83 | var abc = { 84 | hello: 1 85 | ,my :2//comment 86 | ,friend: 3 // comment 87 | } 88 | 89 | // "colon": [0, 1] 90 | // "comment": 2 91 | var abc = { 92 | hello : 1 93 | , my : 2 // comment 94 | , friend: 3 // comment 95 | } 96 | 97 | // "colon": [1, 2] 98 | // "comment": 4 99 | var abc = { 100 | hello : 1 101 | , my : 2 // comment 102 | , friend : 3 // comment 103 | } 104 | 105 | // "colon": [-1, 3] 106 | // "comment": 2 107 | var abc = { 108 | hello: 1 109 | , my: 2 // comment 110 | , friend: 3 // comment 111 | } 112 | 113 | // "colon": [-1, -1] 114 | // "comment": 2 115 | var abc = { 116 | hello:1 117 | , my:2 //comment 118 | , friend:3 // comment 119 | } 120 | 121 | 122 | // Orignal code 123 | $data = array( 124 | 'text' => 'something', 125 | 'here is another' => 'sample' 126 | ); 127 | 128 | // "arrow": [1, 3] 129 | $data = array( 130 | 'text' => 'something', 131 | 'here is another' => 'sample' 132 | ); 133 | 134 | ``` 135 | -------------------------------------------------------------------------------- /images/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WarWithinMe/better-align/d4489612fb9c9782cd9ddde413c6da61f75b30df/images/1.gif -------------------------------------------------------------------------------- /images/2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WarWithinMe/better-align/d4489612fb9c9782cd9ddde413c6da61f75b30df/images/2.gif -------------------------------------------------------------------------------- /images/3.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WarWithinMe/better-align/d4489612fb9c9782cd9ddde413c6da61f75b30df/images/3.gif -------------------------------------------------------------------------------- /images/4.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WarWithinMe/better-align/d4489612fb9c9782cd9ddde413c6da61f75b30df/images/4.gif -------------------------------------------------------------------------------- /images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WarWithinMe/better-align/d4489612fb9c9782cd9ddde413c6da61f75b30df/images/icon.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "better-align", 3 | "displayName": "Better Align", 4 | "description": "Align code without selecting them first.", 5 | "version": "1.1.7", 6 | "icon": "images/icon.png", 7 | "repository": "https://github.com/WarWithinMe/better-align", 8 | "publisher": "wwm", 9 | "engines": { 10 | "vscode": "^1.5.0" 11 | }, 12 | "categ7ries": [ 13 | "Formatters" 14 | ], 15 | "activationEvents": [ 16 | "onCommand:wwm.aligncode" 17 | ], 18 | "main": "./out/src/extension", 19 | "contributes": { 20 | "commands": [ 21 | { 22 | "command": "wwm.aligncode", 23 | "title": "Align" 24 | } 25 | ], 26 | "configuration": { 27 | "type": "object", 28 | "title": "Alignment configuration", 29 | "properties": { 30 | "alignment.operatorPadding": { 31 | "type": "string", 32 | "enum": [ 33 | "left", 34 | "right" 35 | ], 36 | "default": "right", 37 | "description": "Control where to insert space to align different length operators (e.g. aligning = += *= ...)" 38 | }, 39 | "alignment.surroundSpace": { 40 | "type": "object", 41 | "default": { 42 | "colon": [ 43 | 0, 44 | 1 45 | ], 46 | "assignment": [ 47 | 1, 48 | 1 49 | ], 50 | "arrow":[ 51 | 1, 52 | 1 53 | ], 54 | "comment": 2 55 | }, 56 | "description": "Specify how many spaces to insert around the operator." 57 | }, 58 | "alignment.indentBase": { 59 | "type": "string", 60 | "enum": [ 61 | "firstline", 62 | "activeline", 63 | "dontchange" 64 | ], 65 | "default": "firstline", 66 | "description": "firstline: Change indent of all lines to the firstline.\n activeline: Change intent of all lines to the activeline.\n dontchange: Don't change line indent, only aligns those lines with same indentation." 67 | } 68 | } 69 | }, 70 | "configurationDefaults": { 71 | "[shellscript]": { 72 | "alignment.surroundSpace": { 73 | "colon": [ 0, 1 ], 74 | "assignment": [ -1, -1 ], 75 | "arrow":[ 1, 1 ], 76 | "comment": 2 77 | } 78 | } 79 | } 80 | }, 81 | "scripts": { 82 | "vscode:prepublish": "tsc -p ./", 83 | "compile": "tsc -watch -p ./", 84 | "postinstall": "node ./node_modules/vscode/bin/install", 85 | "test": "node ./node_modules/vscode/bin/test" 86 | }, 87 | "devDependencies": { 88 | "typescript": "^2.0.3", 89 | "vscode": "^1.0.0", 90 | "mocha": "^2.3.3", 91 | "@types/node": "^6.0.40", 92 | "@types/mocha": "^2.2.32" 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import * as vscode from 'vscode'; 3 | import Formatter from './formatter'; 4 | 5 | export function activate(context: vscode.ExtensionContext) { 6 | 7 | var formatter = new Formatter(); 8 | 9 | context.subscriptions.push( 10 | vscode.commands.registerTextEditorCommand( "wwm.aligncode", (editor)=>{ 11 | formatter.process( editor ); 12 | } ) 13 | ); 14 | } 15 | 16 | // this method is called when your extension is deactivated 17 | export function deactivate() {} 18 | -------------------------------------------------------------------------------- /src/formatter.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from "vscode"; 2 | 3 | enum TokenType { 4 | Invalid 5 | , Word 6 | , Assignment // = += -= *= /= 7 | , Arrow // => 8 | , Block // {} [] () 9 | , PartialBlock // { [ ( 10 | , EndOfBlock // } ] ) 11 | , String 12 | , PartialString 13 | , Comment 14 | , Whitespace 15 | , Colon 16 | , Comma 17 | , CommaAsWord 18 | , Insertion 19 | } 20 | 21 | interface Token { 22 | type : TokenType; 23 | text : string; 24 | } 25 | 26 | interface LineInfo { 27 | line : vscode.TextLine; 28 | sgfntTokenType : TokenType; 29 | sgfntTokens : TokenType[]; 30 | tokens : Token[]; 31 | } 32 | 33 | interface LineRange { 34 | anchor : number; 35 | infos : LineInfo[]; 36 | } 37 | 38 | const REG_WS = /\s/; 39 | const BRACKET_PAIR = { 40 | "{" : "}" 41 | , "[" : "]" 42 | , "(" : ")" 43 | }; 44 | 45 | function whitespace( count ) { 46 | return new Array(count+1).join(" "); 47 | } 48 | 49 | export default class Formatter { 50 | 51 | /* Align: 52 | * operators = += -= *= /= : 53 | * trailling comment 54 | * preceding comma 55 | * Ignore anything inside a quote, comment, or block 56 | */ 57 | public process( editor:vscode.TextEditor ):void { 58 | this.editor = editor; 59 | 60 | var ranges : LineRange[] = []; 61 | 62 | editor.selections.forEach( (sel)=> { 63 | 64 | const indentBase = this.getConfig().get("indentBase", "firstline") as string; 65 | const importantIndent:boolean = indentBase == "dontchange"; 66 | 67 | let res:LineRange; 68 | if ( sel.isSingleLine ) { 69 | // If this selection is single line. Look up and down to search for the similar neighbour 70 | ranges.push( this.narrow(0, editor.document.lineCount-1, sel.active.line, importantIndent) ); 71 | } else { 72 | // Otherwise, narrow down the range where to align 73 | let start = sel.start.line; 74 | let end = sel.end.line; 75 | 76 | while ( true ) { 77 | res = this.narrow(start, end, start, importantIndent); 78 | let lastLine = res.infos[res.infos.length - 1]; 79 | 80 | if ( lastLine.line.lineNumber > end ) { 81 | break; 82 | } 83 | 84 | if ( res.infos[0] && res.infos[0].sgfntTokenType != TokenType.Invalid ) { 85 | ranges.push( res ); 86 | } 87 | 88 | if ( lastLine.line.lineNumber == end ) { 89 | break; 90 | } 91 | 92 | start = lastLine.line.lineNumber + 1; 93 | } 94 | } 95 | }); 96 | 97 | // Format 98 | let formatted:string[][] = []; 99 | for ( let range of ranges ) { 100 | 101 | /* 102 | console.log( `\n===============` ); 103 | for ( let info of range.infos ) { 104 | console.log( `+++ [${info.line.lineNumber}]: ${TokenType[info.sgfntTokenType]} +++` ); 105 | console.log( info.line, info.tokens ); 106 | } 107 | // */ 108 | 109 | formatted.push( this.format( range ) ); 110 | } 111 | 112 | // Apply 113 | editor.edit(( editBuilder )=>{ 114 | for ( let i = 0; i < ranges.length; ++i ) { 115 | 116 | var infos = ranges[i].infos; 117 | var lastline = infos[infos.length-1].line; 118 | var location = new vscode.Range( infos[0].line.lineNumber, 119 | 0, 120 | lastline.lineNumber, 121 | lastline.text.length ); 122 | 123 | editBuilder.replace(location, formatted[i].join("\n")); 124 | } 125 | }); 126 | } 127 | 128 | protected editor:vscode.TextEditor; 129 | 130 | protected getConfig() { 131 | 132 | let defaultConfig = vscode.workspace.getConfiguration("alignment"); 133 | let langConfig:Object = null; 134 | 135 | try { 136 | langConfig = vscode.workspace.getConfiguration().get(`[${this.editor.document.languageId}]`) as any; 137 | } catch (e) {} 138 | 139 | return { 140 | get : function( key:any, defaultValue?:any ):any { 141 | if ( langConfig ) { 142 | var key1 = "alignment." + key; 143 | if ( langConfig.hasOwnProperty(key1) ) { 144 | return langConfig[key1]; 145 | } 146 | } 147 | 148 | return defaultConfig.get( key, defaultValue ); 149 | } 150 | }; 151 | } 152 | 153 | protected tokenize( line:number ):LineInfo { 154 | let textline = this.editor.document.lineAt( line ); 155 | let text = textline.text; 156 | let pos = 0; 157 | let lt:LineInfo = { 158 | line : textline 159 | , sgfntTokenType : TokenType.Invalid 160 | , sgfntTokens : [] 161 | , tokens : [] 162 | }; 163 | 164 | let lastTokenType = TokenType.Invalid; 165 | let tokenStartPos = -1; 166 | 167 | while ( pos < text.length ) { 168 | 169 | let char = text.charAt(pos); 170 | let next = text.charAt(pos+1); 171 | 172 | let currTokenType:TokenType; 173 | 174 | let nextSeek = 1; 175 | 176 | // Tokens order are important 177 | if ( char.match( REG_WS ) ) { 178 | currTokenType = TokenType.Whitespace; 179 | } else if ( char == "\"" || char == "'" || char == "`" ) { 180 | currTokenType = TokenType.String; 181 | } else if ( char == "{" || char == "(" || char == "[" ) { 182 | currTokenType = TokenType.Block; 183 | } else if ( char == "}" || char == ")" || char == "]" ) { 184 | currTokenType = TokenType.EndOfBlock; 185 | } else if ( char == "/" && ( 186 | (next == "/" && (pos > 0 ? text.charAt(pos-1) : "") != ":") // only `//` but not `://` 187 | || next == "*" 188 | ) ) { 189 | currTokenType = TokenType.Comment; 190 | } else if ( char == ":" && next != ":" ) { 191 | currTokenType = TokenType.Colon; 192 | } else if ( char == "," ) { 193 | if ( lt.tokens.length == 0 || (lt.tokens.length == 1 && lt.tokens[0].type == TokenType.Whitespace) ) { 194 | currTokenType = TokenType.CommaAsWord; // Comma-first style 195 | } else { 196 | currTokenType = TokenType.Comma; 197 | } 198 | } else if ( char == "=" && next == ">" ) { 199 | currTokenType = TokenType.Arrow; 200 | nextSeek = 2; 201 | } else if ( char == "=" && next == "=" ) { 202 | currTokenType = TokenType.Word; 203 | nextSeek = 2; 204 | } else if (( char == "+" || char == "-" || char == "*" || char == "/" ) && next == "=" ) { 205 | currTokenType = TokenType.Assignment; 206 | nextSeek = 2; 207 | } else if ( char == "=" && next != "=" ) { 208 | currTokenType = TokenType.Assignment; 209 | } else { 210 | currTokenType = TokenType.Word; 211 | } 212 | 213 | if ( currTokenType != lastTokenType ) { 214 | if ( tokenStartPos != -1 ) { 215 | lt.tokens.push({ 216 | type : lastTokenType 217 | , text : textline.text.substr(tokenStartPos, pos - tokenStartPos) 218 | }); 219 | } 220 | 221 | lastTokenType = currTokenType; 222 | tokenStartPos = pos; 223 | 224 | if ( lastTokenType == TokenType.Assignment 225 | || lastTokenType == TokenType.Colon 226 | || lastTokenType == TokenType.Arrow ) 227 | { 228 | if ( lt.sgfntTokens.indexOf(lastTokenType) === -1 ) { 229 | lt.sgfntTokens.push( lastTokenType ); 230 | } 231 | } 232 | } 233 | 234 | // Skip to end of string 235 | if ( currTokenType == TokenType.String ) { 236 | ++pos; 237 | while ( pos < text.length ) { 238 | let quote = text.charAt(pos); 239 | if ( quote == char && text.charAt(pos-1) != "\\" ) { 240 | break; 241 | } 242 | ++pos; 243 | } 244 | if ( pos >= text.length ) { 245 | lastTokenType = TokenType.PartialString; 246 | } 247 | } 248 | 249 | // Skip to end of block 250 | if ( currTokenType == TokenType.Block ) { 251 | ++pos; 252 | let bracketCount = 1; 253 | while ( pos < text.length ) { 254 | let bracket = text.charAt(pos); 255 | if ( bracket == char ) { 256 | ++bracketCount; 257 | } else if ( bracket == BRACKET_PAIR[char] && text.charAt(pos-1) != "\\" ) { 258 | if ( bracketCount == 1 ) { 259 | break; 260 | } else { 261 | --bracketCount; 262 | } 263 | } 264 | ++pos; 265 | } 266 | if ( pos >= text.length ) { 267 | lastTokenType = TokenType.PartialBlock; 268 | } 269 | } 270 | 271 | if ( char == "/" ) { 272 | // Skip to end if we encounter single line comment 273 | if ( next == "/" ) { 274 | pos = text.length; 275 | } else if ( next == "*" ) { 276 | ++pos; 277 | while ( pos < text.length ) { 278 | if ( text.charAt(pos) == "*" && text.charAt(pos+1) == "/" ) { 279 | ++pos; 280 | currTokenType = TokenType.Word; 281 | break; 282 | } 283 | ++pos; 284 | } 285 | } 286 | } 287 | 288 | pos += nextSeek; 289 | } 290 | 291 | if ( tokenStartPos != -1 ) { 292 | lt.tokens.push({ 293 | type : lastTokenType 294 | , text : textline.text.substr(tokenStartPos, pos-tokenStartPos) 295 | }); 296 | } 297 | 298 | return lt; 299 | } 300 | 301 | protected hasPartialToken( info:LineInfo ):boolean { 302 | for ( let j = info.tokens.length-1; j >= 0; --j ) { 303 | let lastT = info.tokens[ j ]; 304 | if ( lastT.type == TokenType.PartialBlock 305 | || lastT.type == TokenType.EndOfBlock 306 | || lastT.type == TokenType.PartialString ) 307 | { 308 | return true; 309 | } 310 | } 311 | return false; 312 | } 313 | 314 | protected hasSameIndent( info1:LineInfo, info2:LineInfo ):boolean { 315 | 316 | var t1 = info1.tokens[0]; 317 | var t2 = info2.tokens[0]; 318 | 319 | if ( t1.type == TokenType.Whitespace ) { 320 | if ( t1.text == t2.text ) { 321 | return true; 322 | } 323 | } else if ( t2.type != TokenType.Whitespace ) { 324 | return true; 325 | } 326 | 327 | return false; 328 | } 329 | 330 | protected arrayAnd( array1:TokenType[], array2:TokenType[] ):TokenType[] { 331 | var res:TokenType[] = [] 332 | var map = {} 333 | for ( var i = 0; i < array1.length; ++i ) { 334 | map[array1[i]] = true; 335 | } 336 | for ( var i = 0; i < array2.length; ++i ) { 337 | if ( map[array2[i]] ) { 338 | res.push( array2[i] ); 339 | } 340 | } 341 | return res; 342 | } 343 | 344 | /* 345 | * Determine which blocks of code needs to be align. 346 | * 1. Empty lines is the boundary of a block. 347 | * 2. If user selects something, blocks are always within selection, 348 | * but not necessarily is the selection. 349 | * 3. Bracket / Brace usually means boundary. 350 | * 4. Unsimilar line is boundary. 351 | */ 352 | protected narrow( start:number, end:number, anchor:number, importantIndent:boolean ):LineRange { 353 | let anchorToken = this.tokenize( anchor ); 354 | let range = { anchor, infos:[ anchorToken ] }; 355 | 356 | let tokenTypes = anchorToken.sgfntTokens; 357 | 358 | if ( anchorToken.sgfntTokens.length == 0 ) { 359 | return range; 360 | } 361 | 362 | if ( this.hasPartialToken(anchorToken) ) { 363 | return range; 364 | } 365 | 366 | let i = anchor - 1; 367 | while ( i >= start ) { 368 | let token = this.tokenize( i ); 369 | 370 | if ( this.hasPartialToken(token) ) { 371 | break; 372 | } 373 | 374 | let tt = this.arrayAnd( tokenTypes, token.sgfntTokens ); 375 | if ( tt.length == 0 ) { 376 | break; 377 | } 378 | tokenTypes = tt; 379 | 380 | if ( importantIndent && !this.hasSameIndent(anchorToken, token) ) { 381 | break; 382 | } 383 | 384 | range.infos.unshift( token ); 385 | --i; 386 | } 387 | 388 | i = anchor + 1; 389 | while ( i <= end ) { 390 | let token = this.tokenize(i); 391 | 392 | let tt = this.arrayAnd( tokenTypes, token.sgfntTokens ); 393 | if ( tt.length == 0 ) { 394 | break; 395 | } 396 | tokenTypes = tt; 397 | 398 | if ( importantIndent && !this.hasSameIndent(anchorToken, token) ) { 399 | break; 400 | } 401 | 402 | if ( this.hasPartialToken(token) ) { 403 | range.infos.push( token ); 404 | break; 405 | } 406 | 407 | range.infos.push( token ); 408 | ++i; 409 | } 410 | 411 | let sgt; 412 | if ( tokenTypes.indexOf(TokenType.Assignment) >= 0 ) { 413 | sgt = TokenType.Assignment; 414 | } else { 415 | sgt = tokenTypes[0]; 416 | } 417 | for ( let info of range.infos ) { 418 | info.sgfntTokenType = sgt; 419 | } 420 | 421 | return range; 422 | } 423 | 424 | protected format( range:LineRange ):string[] { 425 | 426 | // 0. Remove indentatioin, and trailing whitespace 427 | let indentation = null; 428 | let anchorLine = range.infos[0]; 429 | const config = this.getConfig(); 430 | 431 | if (config.get("indentBase", "firstline") as string == "activeline") { 432 | for ( let info of range.infos ) { 433 | if ( info.line.lineNumber == range.anchor ) { 434 | anchorLine = info; 435 | break; 436 | } 437 | } 438 | } 439 | 440 | if ( anchorLine.tokens[0].type == TokenType.Whitespace ) { 441 | indentation = anchorLine.tokens[0].text; 442 | } else { 443 | indentation = ""; 444 | } 445 | 446 | for ( let info of range.infos ) { 447 | if ( info.tokens[0].type == TokenType.Whitespace ) { 448 | info.tokens.shift(); 449 | } 450 | if ( info.tokens.length > 1 && info.tokens[ info.tokens.length - 1 ].type == TokenType.Whitespace ) { 451 | info.tokens.pop(); 452 | } 453 | } 454 | 455 | // 1. Special treatment for Word-Word-Operator ( e.g. var abc = ) 456 | let firstWordLength = 0; 457 | for ( let info of range.infos ) { 458 | let count = 0; 459 | for ( let token of info.tokens ) { 460 | if ( token.type == info.sgfntTokenType ) { 461 | count = -count; 462 | break; 463 | } 464 | if ( token.type != TokenType.Whitespace ) { 465 | ++count; 466 | } 467 | } 468 | 469 | if ( count < -1 ) { 470 | firstWordLength = Math.max( firstWordLength, info.tokens[0].text.length ); 471 | } 472 | } 473 | if ( firstWordLength > 0 ) { 474 | let wordSpace: Token = { type: TokenType.Insertion, text: whitespace( firstWordLength + 1 ) }; 475 | let oneSpace: Token = { type: TokenType.Insertion, text: " " }; 476 | 477 | for (let info of range.infos) { 478 | let count = 0; 479 | for (let token of info.tokens) { 480 | if (token.type == info.sgfntTokenType) { 481 | count = -count; 482 | break; 483 | } 484 | if (token.type != TokenType.Whitespace) { 485 | ++count; 486 | } 487 | } 488 | 489 | if (count == -1) { 490 | info.tokens.unshift( wordSpace ); 491 | } else if (count < -1) { 492 | if ( info.tokens[1].type == TokenType.Whitespace ) { 493 | info.tokens[1] = oneSpace; 494 | } else if ( info.tokens[0].type == TokenType.CommaAsWord ) { 495 | info.tokens.splice(1, 0, oneSpace); 496 | } 497 | if ( info.tokens[0].text.length != firstWordLength ) { 498 | let ws = { type: TokenType.Insertion, text: whitespace(firstWordLength-info.tokens[0].text.length) } 499 | if ( info.tokens[0].type == TokenType.CommaAsWord ) { 500 | info.tokens.unshift(ws); 501 | } else { 502 | info.tokens.splice(1,0,ws); 503 | } 504 | } 505 | } 506 | } 507 | } 508 | 509 | // 2. Remove whitespace surrounding operator ( comma in the middle of the line is also consider an operator ). 510 | for ( let info of range.infos ) { 511 | let i = 1; 512 | while ( i < info.tokens.length ) { 513 | if ( info.tokens[i].type == info.sgfntTokenType || info.tokens[i].type == TokenType.Comma ) { 514 | if ( info.tokens[i-1].type == TokenType.Whitespace ) { 515 | info.tokens.splice( i-1, 1 ); 516 | --i; 517 | } 518 | if ( info.tokens[i+1] && info.tokens[i+1].type == TokenType.Whitespace ) { 519 | info.tokens.splice( i+1, 1 ); 520 | } 521 | } 522 | ++i; 523 | } 524 | } 525 | 526 | // 3. Align 527 | const configOP = config.get("operatorPadding") as string; 528 | const configWS = config.get("surroundSpace"); 529 | const stt = TokenType[range.infos[0].sgfntTokenType].toLowerCase(); 530 | const configDef = { "colon": [0, 1], "assignment": [1, 1], "comment": 2, "arrow" : [1, 1] }; 531 | const configSTT = configWS[stt] || configDef[stt]; 532 | const configComment = configWS["comment"] || configDef["comment"]; 533 | 534 | const rangeSize = range.infos.length; 535 | 536 | let length = new Array( rangeSize ); 537 | length.fill(0); 538 | let column = new Array( rangeSize ); 539 | column.fill(0); 540 | let result = new Array( rangeSize ); 541 | result.fill(indentation); 542 | 543 | let exceed = 0; // Tracks how many line have reached to the end. 544 | let hasTrallingComment = false; 545 | let resultSize = 0; 546 | 547 | while ( exceed < rangeSize ) { 548 | 549 | let operatorSize = 0; 550 | 551 | // First pass: for each line, scan until we reach to the next operator 552 | for ( let l = 0; l < rangeSize; ++l ) { 553 | let i = column[l]; 554 | let info = range.infos[l]; 555 | let tokenSize = info.tokens.length; 556 | 557 | if ( i == -1 ) { continue; } 558 | 559 | let end = tokenSize; 560 | let res = result[l]; 561 | 562 | // Bail out if we reach to the trailing comment 563 | if ( tokenSize > 1 && info.tokens[ tokenSize - 1 ].type == TokenType.Comment ) { 564 | hasTrallingComment = true; 565 | if ( tokenSize > 2 && info.tokens[ tokenSize - 2 ].type == TokenType.Whitespace ) { 566 | end = tokenSize - 2; 567 | } else { 568 | end = tokenSize - 1; 569 | } 570 | } 571 | 572 | for ( ; i < end; ++i ) { 573 | let token = info.tokens[i]; 574 | // Vertical align will occur at significant operator or subsequent comma 575 | if ( token.type == info.sgfntTokenType || (token.type == TokenType.Comma && i != 0) ) { 576 | operatorSize = Math.max(operatorSize, token.text.length); 577 | break; 578 | } else { 579 | res += token.text; 580 | } 581 | } 582 | 583 | result[l] = res; 584 | if ( i < end ) { 585 | resultSize = Math.max(resultSize, res.length); 586 | } 587 | 588 | if ( i == end ) { 589 | ++exceed; 590 | column[l] = -1; 591 | info.tokens.splice( 0, end ); 592 | } else { 593 | column[l] = i; 594 | } 595 | } 596 | 597 | // Second pass: align 598 | for ( let l = 0; l < rangeSize; ++l ) { 599 | let i = column[l]; 600 | if ( i == -1 ) { continue; } 601 | 602 | let info = range.infos[l]; 603 | let res = result[l]; 604 | 605 | let op = info.tokens[i].text; 606 | if ( op.length < operatorSize ) { 607 | if ( configOP == "right" ) { 608 | op = whitespace( operatorSize - op.length ) + op; 609 | } else { 610 | op = op + whitespace( operatorSize - op.length ); 611 | } 612 | } 613 | 614 | let padding = ""; 615 | if ( resultSize > res.length ) { 616 | padding = whitespace( resultSize - res.length ); 617 | } 618 | 619 | if ( info.tokens[i].type == TokenType.Comma ) { 620 | res += op; 621 | if ( i < info.tokens.length - 1 ) { 622 | res += padding + " "; // Ensure there's one space after comma. 623 | } 624 | } else { 625 | if (configSTT[0] < 0) { 626 | // operator will stick with the leftside word 627 | if ( configSTT[1] < 0 ) { 628 | // operator will be aligned, and the sibling token will be connected with the operator 629 | let z = res.length - 1; 630 | while ( z >= 0 ) { 631 | let ch = res.charAt( z ); 632 | if ( ch.match(REG_WS) ) { 633 | break; 634 | } 635 | --z; 636 | } 637 | res = res.substring(0, z+1) + padding + res.substring(z+1) + op; 638 | 639 | } else { 640 | res = res + op; 641 | if ( i < info.tokens.length - 1 ) { 642 | res += padding; 643 | } 644 | } 645 | 646 | } else { 647 | res = res + padding + whitespace(configSTT[0]) + op; 648 | } 649 | if (configSTT[1] > 0) { 650 | res += whitespace(configSTT[1]); 651 | } 652 | } 653 | 654 | result[l] = res; 655 | column[l] = i+1; 656 | } 657 | } 658 | 659 | // 4. Align trailing comment 660 | if ( configComment < 0 ) { 661 | // It means user don't want to align trailing comment. 662 | for ( let l = 0; l < rangeSize; ++l ) { 663 | let info = range.infos[l]; 664 | for ( let token of info.tokens ) { 665 | result[l] += token.text; 666 | } 667 | } 668 | } else { 669 | resultSize = 0; 670 | for ( let res of result ) { 671 | resultSize = Math.max( res.length, resultSize ); 672 | } 673 | for ( let l = 0; l < rangeSize; ++l ) { 674 | let info = range.infos[l]; 675 | if ( info.tokens.length ) { 676 | let res = result[l]; 677 | result[l] = res + whitespace(resultSize-res.length+configComment) + info.tokens.pop().text; 678 | } 679 | } 680 | } 681 | 682 | return result; 683 | } 684 | } 685 | -------------------------------------------------------------------------------- /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; -------------------------------------------------------------------------------- /test/testcase.txt: -------------------------------------------------------------------------------- 1 | // Only some comments 2 | // Only some comments 3 | // Only some comments 4 | // Only some comments 5 | // Only some comments 6 | 7 | 8 | var abc = 123; 9 | var fsdafsf = 32423, 10 | fasdf = 1231321; 11 | 12 | 13 | var abc=123; 14 | var fsdafsf=32423, 15 | var fsdafsf=32423, 16 | fasdf=1231321; 17 | fasdf=1231321; 18 | fasdf=1231321; 19 | 20 | 21 | export fdafas=fdasfas; 22 | export fs=fasfdsfadsa; 23 | export fadsfasf=fadsjfkdasf; 24 | export fadsfa=fadfdasfadsf; 25 | 26 | let fdsafa:fadsf = fdsaf; 27 | let lt:LineInfo = { 28 | line:textline 29 | , sgfntTokenType:TokenType.Invalid 30 | , tokens:[] 31 | }; 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "allowUnreachableCode": true, 10 | "sourceMap": true, 11 | "rootDir": "." 12 | }, 13 | "exclude": [ 14 | "node_modules", 15 | ".vscode-test" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /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 --------------------------------------------------------------------------------