├── README.md ├── main.js ├── package.json ├── screenshots ├── screenshot1.png └── screenshot2.png └── tests ├── test.css ├── test.less ├── test2.css └── test2.less /README.md: -------------------------------------------------------------------------------- 1 | #LESShints 2 | > Autocompletion for LESS variables 3 | 4 | This extension for [Brackets](http://brackets.io) gives you hints for LESS variables and shows what these variables actually are. 5 | 6 | ![screenshot](screenshots/screenshot1.png) 7 | 8 | It even has fuzzy search capabilities, so you can for example do this: 9 | 10 | ![screenshot](screenshots/screenshot2.png) 11 | 12 | ##How to use 13 | 14 | 1. Open a *LESS* file 15 | 2. Work with it 16 | 3. Press **@** like you do when you want to insert a LESS variable 17 | 4. Get a list of all variables with their values 18 | 19 | ##How to install 20 | There are three possible ways: 21 | 22 | 1. Install the extension via the Extension Manager in Brackets: ```File -> Extension Manager -> search for 'LESShints'``` 23 | 2. Copy the url of this repository and paste it into ```File -> Extension Manager -> Install from URL``` 24 | 3. [Download the code](https://github.com/konstantinkobs/brackets-LESShints/archive/master.zip) and extract it to the Extensions Folder: ```Help -> Show Extension Folder -> user``` 25 | 26 | ##The MIT License (MIT) 27 | 28 | Copyright (c) 2014 Konstantin Kobs 29 | 30 | Permission is hereby granted, free of charge, to any person obtaining a copy 31 | of this software and associated documentation files (the "Software"), to deal 32 | in the Software without restriction, including without limitation the rights 33 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 34 | copies of the Software, and to permit persons to whom the Software is 35 | furnished to do so, subject to the following conditions: 36 | 37 | The above copyright notice and this permission notice shall be included in 38 | all copies or substantial portions of the Software. 39 | 40 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 41 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 42 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 43 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 44 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 45 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 46 | THE SOFTWARE. -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2014 Konstantin Kobs 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a 5 | * copy of this software and associated documentation files (the "Software"), 6 | * to deal in the Software without restriction, including without limitation 7 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | * and/or sell copies of the Software, and to permit persons to whom the 9 | * Software is furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | * DEALINGS IN THE SOFTWARE. 21 | * 22 | */ 23 | 24 | /*jslint vars: true, plusplus: true, devel: true, nomen: true, indent: 4, maxerr: 50, regexp: true */ 25 | /*global define, brackets, $, window */ 26 | 27 | define(function (require, exports, module) { 28 | "use strict"; 29 | 30 | var AppInit = brackets.getModule("utils/AppInit"), 31 | CodeHintManager = brackets.getModule("editor/CodeHintManager"), 32 | DocumentManager = brackets.getModule("document/DocumentManager"), 33 | LanguageManager = brackets.getModule("language/LanguageManager"), 34 | ProjectManager = brackets.getModule("project/ProjectManager"), 35 | FileUtils = brackets.getModule("file/FileUtils"), 36 | Async = brackets.getModule("utils/Async"); 37 | 38 | 39 | // All file extensions that are supported 40 | var fileextensions = ["less"]; 41 | 42 | /** 43 | * @constructor 44 | */ 45 | function Hint() { 46 | 47 | // Some settings 48 | this.implicitChar = "@"; 49 | this.regex = /@([\w\-]+)\s*:\s*([^\n;]+)/ig; 50 | this.chars = /[@\w\-]/i; 51 | 52 | // Array with hints and the visual list in HTML 53 | this.hints = []; 54 | this.hintsHTML = []; 55 | 56 | // String which was written since the hinter is active 57 | this.writtenSinceStart = ""; 58 | 59 | // Startposition of cursor 60 | this.startPos = null; 61 | 62 | } 63 | 64 | /** 65 | * Checks if it is possible to give hints. 66 | * 67 | * @param {Object} editor The editor object 68 | * @param {String} implicitChar The written character 69 | * @returns {Boolean} whether it is possible to give hints 70 | */ 71 | Hint.prototype.hasHints = function (editor, implicitChar) { 72 | 73 | // The editor instance 74 | this.editor = editor; 75 | 76 | // Set the start position for calculating the written text later 77 | this.startPos = editor.getCursorPos(); 78 | 79 | // Check if the written character is the trigger 80 | return implicitChar ? implicitChar === this.implicitChar : false; 81 | 82 | }; 83 | 84 | /** 85 | * Gets the hints in case there are any 86 | * 87 | * @param {String} implicitChar The last written character 88 | * @returns {Object} The list of hints like brackets wants it 89 | */ 90 | Hint.prototype.getHints = function (implicitChar) { 91 | 92 | // We don't want to give hints if the cursor is out of range 93 | if (!this.validPosition(implicitChar)) { 94 | return null; 95 | } 96 | 97 | // Create the Deferred object to return later 98 | var result = new $.Deferred(); 99 | 100 | // Inside the "done" function we need access to this, 101 | // so we rename it to that. 102 | var that = this; 103 | 104 | // Get the text in the file 105 | this.getText().done(function (text) { 106 | 107 | // Get all matches for the RegExp set earlier 108 | var matches = that.getAll(that.regex, text); 109 | 110 | // Filter the results by everything the user wrote before 111 | matches = that.filterHints(matches); 112 | 113 | // Prepare the hint arrays 114 | that.processHints(matches); 115 | 116 | // Send hints to caller 117 | result.resolve({ 118 | hints: that.hintsHTML, 119 | match: null, 120 | selectInitial: true, 121 | handleWideResults: false 122 | }); 123 | 124 | }); 125 | 126 | return result; 127 | 128 | }; 129 | 130 | /** 131 | * Inserts a chosen hint into the document 132 | * 133 | * @param {String} hint the chosen hint 134 | */ 135 | Hint.prototype.insertHint = function (hint) { 136 | 137 | // We showed the HTML array. Now we need the clean hint. 138 | // Get index from list 139 | var index = this.hintsHTML.indexOf(hint); 140 | // Get hint from index 141 | hint = this.hints[index]; 142 | 143 | // Document instance 144 | var document = DocumentManager.getCurrentDocument(); 145 | 146 | // Endpoint to replace 147 | var pos = this.editor.getCursorPos(); 148 | 149 | // Add text in our document 150 | document.replaceRange(hint, this.startPos, pos); 151 | 152 | }; 153 | 154 | /** 155 | * Checks if it still is possible to give hints. 156 | * It is not possible to give hints anymore if: 157 | * - the cursor is before the position of the starting position 158 | * - the user typed some character which is not usable in a variable name 159 | * 160 | * @param {String} implicitChar The last written character 161 | * @returns {Boolean} True, if the cursor has a valid position 162 | */ 163 | Hint.prototype.validPosition = function (implicitChar) { 164 | 165 | // If the written char is not in a valid 166 | // set of characters for a variable. 167 | if (implicitChar && !this.chars.test(implicitChar)) { 168 | return false; 169 | } 170 | 171 | // Document instance 172 | var document = DocumentManager.getCurrentDocument(); 173 | // Current cursor position 174 | var cursorPos = this.editor.getCursorPos(); 175 | 176 | // If we navigate inside of the range with the cursor 177 | // then we can save the part that was written until now 178 | // Else the cursor is out of range and we don't want to give 179 | // hints anymore. 180 | if (cursorPos.line === this.startPos.line && 181 | cursorPos.ch >= this.startPos.ch) { 182 | this.writtenSinceStart = document.getRange(this.startPos, cursorPos); 183 | } else { 184 | return false; 185 | } 186 | 187 | // If nothing applied until now, we want to pass 188 | return true; 189 | 190 | }; 191 | 192 | /** 193 | * Gets the text of all relevant documents. 194 | * 195 | * @returns {String} Text of all relevant documents (concatenated) 196 | */ 197 | Hint.prototype.getText = function () { 198 | 199 | // Promise for getHints method 200 | var result = new $.Deferred(); 201 | // Contents of all relevant files 202 | var texts = []; 203 | 204 | // Get all relevant files (will be a promise) 205 | ProjectManager.getAllFiles(function (file) { 206 | 207 | // Check if file extension is in the set of supported ones 208 | return (fileextensions.indexOf(FileUtils.getFileExtension(file.fullPath)) !== -1); 209 | 210 | }).done(function (files) { 211 | 212 | // Read all files and push the contents to the texts array 213 | Async.doInParallel(files, function (file) { 214 | 215 | var parallelResult = new $.Deferred(); 216 | 217 | DocumentManager.getDocumentText(file) 218 | .done(function (content) { 219 | 220 | texts.push(content); 221 | 222 | }).always(function () { 223 | 224 | parallelResult.resolve(); 225 | 226 | }); 227 | 228 | return parallelResult.promise(); 229 | 230 | // Give the contents back to caller 231 | }).always(function () { 232 | 233 | result.resolve(texts.join("\n\n")); 234 | 235 | }); 236 | 237 | // If something goes wrong, don't crash! Just do nothing! 238 | }).fail(function () { 239 | 240 | result.resolve(""); 241 | 242 | }).fail(function () { 243 | 244 | result.resolve(""); 245 | 246 | }); 247 | 248 | 249 | return result.promise(); 250 | 251 | }; 252 | 253 | /** 254 | * Returns all matches of the RegExp in the text 255 | * @param {RegExp} regex The RegExp which should be used 256 | * @param {String} text The searchable string 257 | * @returns {Array} All matches of the RegExp in the string 258 | */ 259 | Hint.prototype.getAll = function (regex, text) { 260 | 261 | // We start empty 262 | var matches = []; 263 | 264 | // For every match 265 | var match; 266 | while ((match = regex.exec(text)) !== null) { 267 | 268 | // Push it to the array 269 | matches.push(match); 270 | 271 | } 272 | 273 | // Return the match array 274 | return matches; 275 | 276 | }; 277 | 278 | /** 279 | * Filters the list of hints by the already written part 280 | * 281 | * @param {Array} matches Array of matches 282 | * @returns {Array} the filtered Array 283 | */ 284 | Hint.prototype.filterHints = function (matches) { 285 | 286 | // Split it up/convert to array for fuzzy search 287 | var written = this.writtenSinceStart.toLowerCase().split(""); 288 | 289 | // Filter the matches array 290 | matches = matches.filter(function (match) { 291 | 292 | // Get the hint 293 | var hint = match[1].toLowerCase(); 294 | 295 | // Check if every character of the written string 296 | // is in the right order in the hint 297 | for (var i = 0; i < written.length; i++) { 298 | 299 | var index = hint.indexOf(written[i]); 300 | 301 | if (index === -1) { 302 | return false; 303 | } else { 304 | hint = hint.substr(index + 1); 305 | } 306 | } 307 | 308 | return true; 309 | }); 310 | 311 | // Return the filtered array 312 | return matches; 313 | 314 | }; 315 | 316 | /** 317 | * Processes all the matches and prepares the hints and hintsHTML arrays 318 | * 319 | * @param {Array} matches All the matches (already filtered) 320 | */ 321 | Hint.prototype.processHints = function (matches) { 322 | 323 | // Sort all filtered matches alphabetically 324 | matches = matches.sort(function (match1, match2) { 325 | 326 | var var1 = match1[1].toLowerCase(); 327 | var var2 = match2[1].toLowerCase(); 328 | 329 | if (var1 > var2) { 330 | return 1; 331 | } else if (var1 < var2) { 332 | return -1; 333 | } else { 334 | return 0; 335 | } 336 | 337 | }); 338 | 339 | // Put every hint for insertion in the hints array 340 | this.hints = matches.map(function (match) { 341 | return match[1]; 342 | }); 343 | 344 | // Create the hintsHTML array which will be shown to the 345 | // user. It has a preview of what the variable is set to. 346 | this.hintsHTML = matches.map(function (match) { 347 | return match[1] + "" + match[2] + ""; 348 | }); 349 | 350 | }; 351 | 352 | /** 353 | * Register the HintProvider 354 | */ 355 | AppInit.appReady(function () { 356 | var hints = new Hint(); 357 | CodeHintManager.registerHintProvider(hints, fileextensions, 0); 358 | }); 359 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lesshints", 3 | "title": "LESSHints", 4 | "author": "Konstantin Kobs", 5 | "homepage": "https://github.com/konstantinkobs/brackets-LESShints", 6 | "version": "1.1.0", 7 | "engines": { "brackets": ">=0.23" }, 8 | "description": "Autocompletion for LESS variables." 9 | } -------------------------------------------------------------------------------- /screenshots/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstantinkobs/brackets-LESShints/200beb70efa1a2ceed9fcf34916b98629a34fbbb/screenshots/screenshot1.png -------------------------------------------------------------------------------- /screenshots/screenshot2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/konstantinkobs/brackets-LESShints/200beb70efa1a2ceed9fcf34916b98629a34fbbb/screenshots/screenshot2.png -------------------------------------------------------------------------------- /tests/test.css: -------------------------------------------------------------------------------- 1 | /* Generated by less 1.7.5 */ 2 | body { 3 | background: #008000; 4 | } 5 | -------------------------------------------------------------------------------- /tests/test.less: -------------------------------------------------------------------------------- 1 | @zbackground: green; 2 | @Foreground: rgb(1, 50, 140); 3 | @font: "Helvetica Neue", Helvetica, Arial; 4 | 5 | body{ 6 | background: 7 | } -------------------------------------------------------------------------------- /tests/test2.css: -------------------------------------------------------------------------------- 1 | /* Generated by less 2.2.0 */ 2 | -------------------------------------------------------------------------------- /tests/test2.less: -------------------------------------------------------------------------------- 1 | @zbackground2: blue; 2 | @Foreground2: #ccc; 3 | @font2: Arial; 4 | 5 | html{ 6 | font-family: 7 | } --------------------------------------------------------------------------------