├── .gitignore ├── .gitattributes ├── assets ├── logo.png ├── preview.gif ├── sponsor.png └── preview-config.gif ├── .github └── FUNDING.yml ├── .vscodeignore ├── .vscode ├── settings.json ├── extensions.json └── launch.json ├── jsconfig.json ├── .eslintrc.json ├── CHANGELOG.md ├── README.md ├── package.json └── extension.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .vscode-test/ 3 | *.vsix 4 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | 4 | -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnurkarim/HTML-to-CSS-autocompletion/HEAD/assets/logo.png -------------------------------------------------------------------------------- /assets/preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnurkarim/HTML-to-CSS-autocompletion/HEAD/assets/preview.gif -------------------------------------------------------------------------------- /assets/sponsor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnurkarim/HTML-to-CSS-autocompletion/HEAD/assets/sponsor.png -------------------------------------------------------------------------------- /assets/preview-config.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solnurkarim/HTML-to-CSS-autocompletion/HEAD/assets/preview-config.gif -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | patreon: solnurkarim 4 | custom: https://www.paypal.me/htmltocss 5 | -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | .gitignore 5 | jsconfig.json 6 | vsc-extension-quickstart.md 7 | .eslintrc.json 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "typescript.surveys.enabled": false 4 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "checkJs": true, /* Typecheck .js files. */ 6 | "lib": [ 7 | "es6" 8 | ] 9 | }, 10 | "exclude": [ 11 | "node_modules" 12 | ] 13 | } -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": false, 4 | "commonjs": true, 5 | "es6": false, 6 | "node": true 7 | }, 8 | "parserOptions": { 9 | "ecmaFeatures": { 10 | "jsx": true 11 | }, 12 | "sourceType": "module" 13 | }, 14 | "rules": { 15 | "no-const-assign": "warn", 16 | "no-this-before-super": "warn", 17 | "no-undef": "warn", 18 | "no-unreachable": "warn", 19 | "no-unused-vars": "warn", 20 | "constructor-super": "warn", 21 | "valid-typeof": "warn" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.1.0 2 | - Added configuration of `trigger keys`. If `enabled`, type `#` or `.` to show completion item list 3 | - Fixed a bug when completion items have been visible within property lines with multiple values 4 | 5 | ## 0.0.4 6 | - Extension is now back alive 7 | - Completions are now being provided in a proper manner, that is not showing within url paths and all 8 | 9 | ## 0.0.3 10 | - Removed option to configure stylesheet file types. Extension will now be activating on `CSS`, `SCSS`, `Sass`, `Less` and `Stylus` stylesheets opening 11 | - As `Sass` and `Stylus` stylesheets are not supported by `VSCode` by default it is highly advisable to install `Sass`/`Stylus` language support extensions first for this extension to be working properly 12 | - Extension removal notice added 13 | 14 | ## 0.0.2 15 | - Additional documentation information provided 16 | 17 | ## 0.0.1 18 | - Initial release -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that launches the extension inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "type": "extensionHost", 10 | "request": "launch", 11 | "name": "Launch Extension", 12 | "runtimeExecutable": "${execPath}", 13 | "args": ["--extensionDevelopmentPath=${workspaceFolder}"], 14 | "outFiles": ["${workspaceFolder}/out/**/*.js"], 15 | "preLaunchTask": "npm" 16 | }, 17 | { 18 | "name": "Extension", 19 | "type": "extensionHost", 20 | "request": "launch", 21 | "runtimeExecutable": "${execPath}", 22 | "args": [ 23 | "--extensionDevelopmentPath=${workspaceFolder}", 24 | "--disable-extensions" 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # HTML to CSS completion suggestions 2 | 3 | Default settings are set to `html`/`php` >> `css`/`scss`/`less`/`sass`/`stylus` flow. 4 | To change file types you want to get selectors from use `HTML to CSS autocompletion` extension configuration from `command palette` or VSCode `user settings`. 5 | 6 | ![preview](assets/preview.gif) 7 | 8 | ## Extension features 9 | 10 | - Intellisense suggestions/completions for `classes` and `ids` from `markup` documents to `CSS`, `SCSS`, `Less`, `Sass`, `Stylus` stylesheets 11 | - Configuration of `file types`, `files`, `folders` or `workspaces` to work with 12 | 13 | ## How to configure 14 | 15 | - Enter `HTML to CSS autocompletion: Extension Configuration` from the command palette 16 | - Or find `html-to-css-autocompletion` configuration entries in VSCode user settings 17 | 18 | ![preview](assets/preview-config.gif) 19 | 20 | ## Configuration options 21 | 22 | - `html-to-css-autocompletion.triggerCharacters` 23 | Shows completion list only on '#'/'.' character entries. Default: `disable` 24 | 25 | - `html-to-css-autocompletion.autocompletionFilesScope` 26 | Defines scope for extension to work with. `Options`: 27 | `multi-root`: all selectors found within all root folders will be visible to defined stylesheets. This is `Default` autocompletion provider's scope. 28 | `workspace`: all selectors found within particular workspace folder/project will be visible to stylesheets within that workspace folder. 29 | `linked files`: selectors will be provided only for linked stylesheets. 30 | 31 | - `html-to-css-autocompletion.getSelectorsFromFileTypes` 32 | Defines file types to be searched for classes and ids. Default: `html, php` 33 | 34 | - `html-to-css-autocompletion.folderNamesToBeIncluded` 35 | Defines only specific folder names to be searched. Default: `''` 36 | 37 | - `html-to-css-autocompletion.folderNamesToBeExcluded` 38 | Defines folder names to be excluded from being searched. Default: `node_modules` 39 | 40 | - `html-to-css-autocompletion.includePattern` 41 | Set custom glob pattern to get classes/ids from matched files. E.g.: `**/{folderName1,folderName2,...}/*.{fileType1,fileType2,...}` 42 | 43 | - `html-to-css-autocompletion.excludePattern` 44 | Set custom glob pattern to exclude search on pattern matches. E.g.: `**/{folderName1,folderName2,...}/**` 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "html-to-css-autocompletion", 3 | "displayName": "HTML to CSS autocompletion", 4 | "description": "Provides completion suggestions for classes and ids from markup documents to stylesheets.", 5 | "version": "1.1.2", 6 | "publisher": "solnurkarim", 7 | "engines": { 8 | "vscode": "^1.25.0" 9 | }, 10 | "license": "SEE LICENSE IN LICENSE.txt", 11 | "bugs": { 12 | "url": "https://github.com/solnurkarim/HTML-to-CSS-autocompletion/issues", 13 | "email": "solnurkarim@gmail.com" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/solnurkarim/HTML-to-CSS-autocompletion.git" 18 | }, 19 | "icon": "assets/logo.png", 20 | "categories": [ 21 | "Programming Languages", 22 | "Other" 23 | ], 24 | "activationEvents": [ 25 | "onLanguage:css", 26 | "onLanguage:scss", 27 | "onLanguage:less", 28 | "onLanguage:sass", 29 | "onLanguage:styl" 30 | ], 31 | "contributes": { 32 | "configuration": { 33 | "title": "HTML to CSS autocompletion extension configuration", 34 | "properties": { 35 | "html-to-css-autocompletion.triggerCharacters": { 36 | "type": "boolean", 37 | "default": false, 38 | "description": "Set true to populate Intellisense with selectors only when '#' or '.' characters are typed. Default: false" 39 | }, 40 | "html-to-css-autocompletion.autocompletionFilesScope": { 41 | "type": "string", 42 | "enum": [ 43 | "multi-root", 44 | "workspace", 45 | "linked-files" 46 | ], 47 | "default": "multi-root", 48 | "description": "Defines scopes for extension to work with.\r\nmulti-root: all selectors found within all root folders will be visible to all stylesheets.\r\nworkspace: all selectors found within particular workspace folder/project will be visible to stylesheets within that workspace folder.\r\nlinked files: selectors will be provided only for linked stylesheets.\r\nDefault: \"multi-root\"" 49 | }, 50 | "html-to-css-autocompletion.getSelectorsFromFileTypes": { 51 | "type": "array", 52 | "default": [ 53 | "html", 54 | "php" 55 | ], 56 | "description": "Defines file types to be searched for classes/ids.\r\nDefaults: [\"html\", \"php\"]" 57 | }, 58 | "html-to-css-autocompletion.folderNamesToBeIncluded": { 59 | "type": "array", 60 | "default": [ 61 | "" 62 | ], 63 | "description": "Defines only specific folder names to be searched. Default: [\"\"]" 64 | }, 65 | "html-to-css-autocompletion.folderNamesToBeExcluded": { 66 | "type": "array", 67 | "default": [ 68 | "node_modules" 69 | ], 70 | "description": "Defines folder names to be excluded from being searched. Default: [\"node_modules\"]" 71 | }, 72 | "html-to-css-autocompletion.includePattern": { 73 | "type": "string", 74 | "default": "", 75 | "description": "Set custom glob pattern to get classes/ids from matched files. E.g.: **/{folderName1,folderName2,...}/*.{fileType1,fileType2,...}" 76 | }, 77 | "html-to-css-autocompletion.excludePattern": { 78 | "type": "string", 79 | "default": "", 80 | "description": "Set custom glob pattern to exclude search on pattern matches. E.g.: **/{folderName1,folderName2,...}/**" 81 | } 82 | } 83 | }, 84 | "commands": [ 85 | { 86 | "command": "htmlToCssConfig", 87 | "title": "Extension Configuration", 88 | "category": "HTML to CSS autocompletion" 89 | } 90 | ] 91 | }, 92 | "main": "./extension", 93 | "scripts": { 94 | "postinstall": "node ./node_modules/vscode/bin/install", 95 | "test": "node ./node_modules/vscode/bin/test" 96 | }, 97 | "devDependencies": { 98 | "typescript": "^2.6.1", 99 | "vscode": "^1.1.6", 100 | "eslint": "^4.11.0", 101 | "@types/node": "^7.0.43", 102 | "@types/mocha": "^2.2.42" 103 | }, 104 | "dependencies": {} 105 | } -------------------------------------------------------------------------------- /extension.js: -------------------------------------------------------------------------------- 1 | const { 2 | workspace, 3 | window, 4 | languages, 5 | commands, 6 | Uri, 7 | MarkdownString, 8 | RelativePattern, 9 | CompletionItem, 10 | Position, 11 | Range, 12 | Hover 13 | } = require('vscode'); 14 | const fs = require('fs'); 15 | const Path = require('path'); 16 | 17 | // get configuration settings from package.json on extension startup 18 | let config = workspace.getConfiguration('html-to-css-autocompletion'); 19 | 20 | // VSCode extension API activate function 21 | function activate(context) { 22 | // check if any workspaces/folders opened 23 | if (workspace.workspaceFolders.length !== 0) { 24 | getPathList(); 25 | registerProviders(); 26 | 27 | // register command palette configuration 28 | const configCommand = commands.registerCommand('htmlToCssConfig', async function() { 29 | // show configuration menu UI on command activation 30 | const configMenuInput = await window.showQuickPick(Object.keys(configInputMethods)); 31 | 32 | // start handler for chosen setting 33 | if (configMenuInput) { 34 | configMenuInput === 'Restore configurations to default' 35 | ? configInputMethods[configMenuInput]() 36 | : await configInputMethods[configMenuInput].set(); 37 | if (configInput) { 38 | window.showInformationMessage('HTML to CSS autocompletion: Configuration changes are now active.'); 39 | configInput = ''; 40 | } 41 | } 42 | }); 43 | 44 | // fire handler when extension configuration has been changed from command palette or user settings 45 | const configWatcher = workspace.onDidChangeConfiguration((e) => { 46 | if (e.affectsConfiguration('html-to-css-autocompletion')) { 47 | config = workspace.getConfiguration('html-to-css-autocompletion'); 48 | if (!isActiveRestoreAllConfigs) { 49 | if (e.affectsConfiguration('html-to-css-autocompletion.autocompletionFilesScope')) 50 | providerScope = config.get('autocompletionFilesScope'); 51 | if (e.affectsConfiguration('html-to-css-autocompletion.triggerCharacters')) registerProviders(); 52 | // fetch files and selectors based on new config settings 53 | if ( 54 | e.affectsConfiguration('html-to-css-autocompletion.getSelectorsFromFileTypes') || 55 | e.affectsConfiguration('html-to-css-autocompletion.folderNamesToBeIncluded') || 56 | e.affectsConfiguration('html-to-css-autocompletion.folderNamesToBeExcluded') || 57 | e.affectsConfiguration('html-to-css-autocompletion.includePattern') || 58 | e.affectsConfiguration('html-to-css-autocompletion.excludePattern') 59 | ) 60 | getPathList(); 61 | } 62 | } 63 | }); 64 | 65 | // update 'files' object when workspace is added or removed 66 | const workspaceWatcher = workspace.onDidChangeWorkspaceFolders((e) => { 67 | if (e.added) { 68 | const newWorkspaceFolders = e.added; 69 | for (let index in newWorkspaceFolders) { 70 | const folder = newWorkspaceFolders[index]; 71 | getPathList(folder.uri.fsPath); 72 | } 73 | } 74 | 75 | if (e.removed) { 76 | const removedWorkspaceFolders = e.removed; 77 | for (let index in removedWorkspaceFolders) { 78 | const folder = removedWorkspaceFolders[index]; 79 | for (let obj in files) { 80 | if (files[obj].workspaceFolder === folder.uri.fsPath) { 81 | delete files[obj]; 82 | } 83 | } 84 | } 85 | } 86 | }); 87 | 88 | // will dispose watchers/listeners on extension deactivation or VScode exit 89 | context.subscriptions.push(configCommand); 90 | context.subscriptions.push(configWatcher); 91 | context.subscriptions.push(workspaceWatcher); 92 | } 93 | } 94 | exports.activate = activate; 95 | 96 | let providerScope = config.get('autocompletionFilesScope'); 97 | let files = {}; 98 | let watchers = []; 99 | let providers = []; 100 | let isActiveRestoreAllConfigs = false; 101 | 102 | // get all file paths if no workspace has been passed 103 | function getPathList(workspaceFolder) { 104 | if (!workspaceFolder) files = {}; 105 | 106 | // get from user settings which paths to include/exclude 107 | const includeFoldersConfig = config.get('folderNamesToBeIncluded'); 108 | const excludeFoldersConfig = config.get('folderNamesToBeExcluded'); 109 | const getFromFileTypesConfig = config.get('getSelectorsFromFileTypes'); 110 | const includeConfig = config.get('includePattern'); 111 | const excludeConfig = config.get('excludePattern'); 112 | 113 | const fileTypesStr = getFromFileTypesConfig.reduce((str, fileType, ind) => { 114 | fileType = fileType.trim(); 115 | return ind === getFromFileTypesConfig.length - 1 && getFromFileTypesConfig.length > 0 116 | ? '{' + str + ',' + fileType + '}' 117 | : (str += ',' + fileType); 118 | }); 119 | 120 | let includeStr; 121 | let include; 122 | if (workspaceFolder) { 123 | if (includeConfig) { 124 | include = new RelativePattern(workspaceFolder, includeConfig); 125 | } else if (includeFoldersConfig[0]) { 126 | includeStr = includeFoldersConfig.reduce((str, folder, ind) => { 127 | return ind === includeFoldersConfig.length - 1 && includeFoldersConfig.length > 0 128 | ? '{' + str + ',' + folder + '}' 129 | : (str += ',' + folder); 130 | }); 131 | include = new RelativePattern(workspaceFolder, `**/${includeStr}/*.${fileTypesStr}`); 132 | } else { 133 | include = new RelativePattern(workspaceFolder, `**/*.${fileTypesStr}`); 134 | } 135 | } else { 136 | if (includeConfig) { 137 | include = includeConfig; 138 | } else if (includeFoldersConfig[0]) { 139 | includeStr = includeFoldersConfig.reduce((str, folder, ind) => { 140 | return ind === includeFoldersConfig.length - 1 && includeFoldersConfig.length > 0 141 | ? '{' + str + ',' + folder + '}' 142 | : (str += ',' + folder); 143 | }); 144 | include = `**/${includeStr}/*.${fileTypesStr}`; 145 | } else { 146 | include = `**/*.${fileTypesStr}`; 147 | } 148 | } 149 | 150 | let excludeStr; 151 | let exclude; 152 | 153 | if (excludeConfig) { 154 | exclude = excludeConfig; 155 | } else { 156 | excludeStr = excludeFoldersConfig.reduce((str, folder, ind) => { 157 | return ind === excludeFoldersConfig.length - 1 && excludeFoldersConfig.length > 0 158 | ? '{' + str + ',' + folder + '}' 159 | : (str += ',' + folder); 160 | }); 161 | exclude = `**/${excludeStr}/**`; 162 | } 163 | 164 | // create file object for each resolved path 165 | workspace.findFiles(include, exclude, 100).then( 166 | (data) => { 167 | data.forEach((uri) => { 168 | createFileObject(uri); 169 | }); 170 | getFiles(); 171 | }, 172 | (err) => console.log(new Error(err)) 173 | ); 174 | 175 | // set change/delete watcher for included files 176 | setFSWatcher(include); 177 | } 178 | 179 | // create object from the given path in 'files' 180 | function createFileObject(uri) { 181 | const path = uri.fsPath; 182 | files[path] = { 183 | workspaceFolder: workspace.getWorkspaceFolder(uri).uri.fsPath, 184 | path: path, 185 | isProcessing: false, 186 | data: null, 187 | selectors: {}, 188 | stylesheets: [] 189 | }; 190 | } 191 | 192 | // get data from paths 193 | function getFiles(fileChange) { 194 | // get data from all paths in 'files' if no particular path has been received 195 | if (fileChange) { 196 | readFile(fileChange); 197 | } else { 198 | for (let obj in files) { 199 | //skip if path has already been read 200 | if (files[obj].data) continue; 201 | const path = files[obj].path; 202 | readFile(path); 203 | } 204 | } 205 | } 206 | 207 | // get data from path then send it to parser 208 | function readFile(path) { 209 | fs.readFile(path, 'utf8', (err, file) => { 210 | if (err) { 211 | console.log(err); 212 | } 213 | files[path].data = file; 214 | parseData(files[path]); 215 | }); 216 | files[path].isProcessing = true; 217 | } 218 | 219 | // get classes/ids and stylesheet paths from given file object 220 | function parseData(fileObj) { 221 | fileObj.selectors = {}; 222 | fileObj.stylesheets = []; 223 | const file = fileObj.data; 224 | let regex = /(class|id|rel=["'].*(stylesheet).*["'].+href)=["']([^"']+)["']/gi; 225 | let match; 226 | let selector; 227 | 228 | while ((match = regex.exec(file))) { 229 | if (match[1] === 'class') { 230 | let matchArr = match[3].split(' '); 231 | for (let index in matchArr) { 232 | selector = '.' + matchArr[index]; 233 | setFileObjectSelectors(fileObj, selector); 234 | } 235 | } else if (match[1] === 'id') { 236 | selector = '#' + match[3]; 237 | setFileObjectSelectors(fileObj, selector); 238 | } else if (match[2] === 'stylesheet') { 239 | const stylesheet = Path.resolve(Path.dirname(fileObj.path), match[3]); 240 | fileObj.stylesheets.push(stylesheet); 241 | } 242 | // else console.log(new Error('Unexpected pattern match: ' + match[0])); 243 | } 244 | 245 | fileObj.isProcessing = false; 246 | } 247 | 248 | // set selectors within each file/path object 249 | function setFileObjectSelectors(fileObject, selector) { 250 | fileObject.selectors.hasOwnProperty(selector) 251 | ? fileObject.selectors[selector]++ 252 | : (fileObject.selectors[selector] = 1); 253 | } 254 | 255 | // get each selector and some data about it from file/path object and store it in received object reference 256 | function getFileObjectSelectors(fileObj, selectorsObj) { 257 | for (let selector in fileObj.selectors) { 258 | if (selectorsObj.hasOwnProperty(selector)) { 259 | selectorsObj[selector].count += fileObj.selectors[selector]; 260 | selectorsObj[selector].files.push({ 261 | uri: Uri.file(fileObj.path), 262 | relativePath: workspace.asRelativePath(fileObj.path, true) 263 | }); 264 | } else { 265 | selectorsObj[selector] = { 266 | selector: selector, 267 | count: fileObj.selectors[selector], 268 | files: [ 269 | { 270 | uri: Uri.file(fileObj.path), 271 | relativePath: workspace.asRelativePath(fileObj.path, true) 272 | } 273 | ] 274 | }; 275 | } 276 | } 277 | } 278 | 279 | function registerItemProvider(languageFilter) { 280 | const triggerCharsBool = config.get('triggerCharacters'); 281 | 282 | const providerFunction = { 283 | provideCompletionItems: (document, position, token, context) => { 284 | // check if provider was invoked by trigger character || document is minified 285 | if ((triggerCharsBool && context.triggerKind != 1) || position.character > 100) return; 286 | 287 | // check if cursor position is not within property line 288 | const start = new Position(position.line, 0); 289 | const range = new Range(start, position); 290 | const lineText = document.getText(range); 291 | 292 | for (let i = lineText.length - 1; i > 0; i--) { 293 | if (lineText[i] === ':' && lineText[i + 1] === ' ') return; 294 | } 295 | 296 | const items = []; 297 | // get selectors within defined extension scope 298 | const scopedSelectors = getScopedSelectors(document); 299 | 300 | // create completion items 301 | for (let selector in scopedSelectors) { 302 | const selectorObj = scopedSelectors[selector]; 303 | const item = new CompletionItem(selector); 304 | // set count and source data for given selector 305 | item.documentation = getSelectorData(selectorObj); 306 | // set icon 307 | item.kind = 13; 308 | items.push(item); 309 | } 310 | 311 | return items; 312 | } 313 | }; 314 | 315 | // register provider with or without trigger characters 316 | let completionProvider; 317 | if (triggerCharsBool) { 318 | completionProvider = languages.registerCompletionItemProvider( 319 | languageFilter, 320 | providerFunction, 321 | ...[ '#', '.' ] 322 | ); 323 | } else { 324 | completionProvider = languages.registerCompletionItemProvider(languageFilter, providerFunction); 325 | } 326 | 327 | providers.push(completionProvider); 328 | } 329 | 330 | function registerHoverProvider(languageFilter) { 331 | const hoverProvider = languages.registerHoverProvider(languageFilter, { 332 | provideHover: (document, position, token) => { 333 | // get word at mouse pointer and return information about it if it's a class or id 334 | const start = new Position(position.line, 0); 335 | const end = new Position(position.line + 1, 0); 336 | const range = new Range(start, end); 337 | const line = document.getText(range); 338 | const selectorLeft = line.slice(null, position.character).match(/[\#\.][\w_-]*$/); 339 | const selectorRight = line.slice(position.character).match(/^[\w_-]*/); 340 | const selector = selectorLeft[0] + selectorRight[0]; 341 | 342 | // get selectors within defined extension scope 343 | const scopedSelectors = getScopedSelectors(document); 344 | 345 | if (scopedSelectors.hasOwnProperty(selector)) { 346 | const content = getSelectorData(scopedSelectors[selector]); 347 | const hover = new Hover(content); 348 | return hover; 349 | } else return null; 350 | } 351 | }); 352 | providers.push(hoverProvider); 353 | } 354 | 355 | // register autocompletion and mouse hover providers 356 | function registerProviders() { 357 | // dispose of already registered providers 358 | if (providers.length > 0) removeDisposables(providers); 359 | // set file types to provide completions to 360 | const languageFilter = { 361 | scheme: 'file', 362 | pattern: '**/*.{css,scss,less,sass,styl}' 363 | }; 364 | registerItemProvider(languageFilter); 365 | registerHoverProvider(languageFilter); 366 | } 367 | 368 | // check which extension scope is defined and return selectors from files within that scope 369 | function getScopedSelectors(document) { 370 | const workspaceFolder = workspace.getWorkspaceFolder(document.uri).uri.fsPath; 371 | let scopedSelectors = {}; 372 | 373 | // get selectors within particular workspace folder 374 | if (providerScope === 'workspace') { 375 | for (let obj in files) { 376 | const file = files[obj]; 377 | if (file.workspaceFolder === workspaceFolder) { 378 | getFileObjectSelectors(file, scopedSelectors); 379 | } 380 | } 381 | } else if (providerScope === 'linked-files') { 382 | // get selectors from files where active stylesheet has been defined within tag 383 | for (let obj in files) { 384 | const file = files[obj]; 385 | for (let index in file.stylesheets) { 386 | if (file.stylesheets[index] === document.uri.fsPath) { 387 | getFileObjectSelectors(file, scopedSelectors); 388 | break; 389 | } 390 | } 391 | } 392 | } else { 393 | // get all project selectors 394 | for (let obj in files) { 395 | const file = files[obj]; 396 | getFileObjectSelectors(file, scopedSelectors); 397 | } 398 | } 399 | 400 | return scopedSelectors; 401 | } 402 | 403 | // set count and source data for completion/hover item 404 | function getSelectorData(selectorObj) { 405 | let itemDoc = new MarkdownString( 406 | '`' + selectorObj.selector + '`\r\n\r\n' + selectorObj.count + ' occurences in files:\r\n\r\n' 407 | ); 408 | for (let index in selectorObj.files) { 409 | const pathObj = selectorObj.files[index]; 410 | itemDoc.appendMarkdown('\r\n\r\n[' + pathObj.relativePath + '](' + pathObj.uri + ')'); 411 | } 412 | 413 | return itemDoc; 414 | } 415 | 416 | // update file object data on file change/create/delete 417 | function setFSWatcher(includePattern) { 418 | // dispose of already registered watchers 419 | if (watchers.length > 0) removeDisposables(watchers); 420 | const globWatcher = workspace.createFileSystemWatcher(includePattern); 421 | watchers.push(globWatcher); 422 | 423 | globWatcher.onDidChange((uri) => { 424 | if (files.hasOwnProperty(uri.fsPath) && files[uri.fsPath].isProcessing) return; 425 | getFiles(uri.fsPath); 426 | }); 427 | 428 | globWatcher.onDidDelete((uri) => { 429 | delete files[uri.fsPath]; 430 | }); 431 | 432 | globWatcher.onDidCreate((uri) => { 433 | createFileObject(uri); 434 | getFiles(uri.fsPath); 435 | }); 436 | } 437 | 438 | /** 439 | * config section 440 | */ 441 | 442 | let configInput; 443 | 444 | // show specified configuration input UI and update config or ask to restore config if no data provided 445 | const configInputMethods = { 446 | 'Toggle trigger keys': { 447 | configName: 'triggerCharacters', 448 | defaultVal: false, 449 | set: async function() { 450 | configInput = await window.showQuickPick([ 'Enable', 'Disable' ], { 451 | placeHolder: "Shows completion list only on '#'/'.' character entries." 452 | }); 453 | if (configInput === 'Enable') config.update(this.configName, true); 454 | else config.update(this.configName, false); 455 | }, 456 | toDefault: function() { 457 | config.update(this.configName, this.defaultVal); 458 | } 459 | }, 460 | 'Set autocompletion workspace scope': { 461 | configName: 'autocompletionFilesScope', 462 | defaultVal: 'multi-root', 463 | set: async function() { 464 | configInput = await window.showQuickPick([ 'multi-root', 'workspace', 'linked-files' ]); 465 | if (configInput) updateConfig(this.configName, configInput); 466 | }, 467 | toDefault: function() { 468 | updateConfig(this.configName, this.defaultVal); 469 | } 470 | }, 471 | 'Set file types to be searched for classes/ids': { 472 | configName: 'getSelectorsFromFileTypes', 473 | defaultVal: 'html,php', 474 | set: async function() { 475 | configInput = await window.showInputBox({ 476 | prompt: 'Set file types to be searched for classes/ids. E.g.: html, php', 477 | placeHolder: 'html' 478 | }); 479 | 480 | if (configInput) updateConfig(this.configName, configInput); 481 | else if (configInput === '') askToDefault(this); 482 | }, 483 | toDefault: function() { 484 | updateConfig(this.configName, this.defaultVal); 485 | } 486 | }, 487 | 'Set list of include folders': { 488 | configName: 'folderNamesToBeIncluded', 489 | defaultVal: '', 490 | set: async function() { 491 | configInput = await window.showInputBox({ 492 | prompt: 'Sets folders to be searched for file types. E.g.: app, folderName, folderName2' 493 | }); 494 | 495 | if (configInput) updateConfig(this.configName, configInput); 496 | else if (configInput === '') askToDefault(this); 497 | }, 498 | toDefault: function() { 499 | updateConfig(this.configName, this.defaultVal); 500 | } 501 | }, 502 | 'Set list of exclude folders': { 503 | configName: 'folderNamesToBeExcluded', 504 | defaultVal: 'node_modules', 505 | set: async function() { 506 | configInput = await window.showInputBox({ 507 | prompt: 'Sets folders to be excluded from searching. E.g.: app, folderName, folderName2' 508 | }); 509 | 510 | if (configInput) updateConfig(this.configName, configInput); 511 | else if (configInput === '') askToDefault(this); 512 | }, 513 | toDefault: function() { 514 | updateConfig(this.configName, this.defaultVal); 515 | } 516 | }, 517 | 'Set include glob pattern': { 518 | configName: 'includePattern', 519 | defaultVal: '', 520 | set: async function() { 521 | configInput = await window.showInputBox({ 522 | prompt: 'Set include glob pattern. E.g.: **/{folderName1,folderName2,...}/*.{fileType1,fileType2,...}' 523 | }); 524 | 525 | if (configInput) updateConfig(this.configName, configInput); 526 | else if (configInput === '') askToDefault(this); 527 | }, 528 | toDefault: function() { 529 | updateConfig(this.configName, this.defaultVal); 530 | } 531 | }, 532 | 'Set exclude glob pattern': { 533 | configName: 'excludePattern', 534 | defaultVal: '', 535 | set: async function() { 536 | configInput = await window.showInputBox({ 537 | prompt: 'Set exclude glob pattern. E.g.: **/{folderName1,folderName2,...}/**' 538 | }); 539 | 540 | if (configInput) updateConfig(this.configName, configInput); 541 | else if (configInput === '') askToDefault(this); 542 | }, 543 | toDefault: function() { 544 | updateConfig(this.configName, this.defaultVal); 545 | } 546 | }, 547 | // restore all extension configurations to default, renew files data and re-register providers 548 | 'Restore configurations to default': function() { 549 | isActiveRestoreAllConfigs = true; 550 | const configOptions = Object.keys(this); 551 | for (let i = 0; i < configOptions.length - 1; i++) { 552 | this[configOptions[i]].toDefault(); 553 | } 554 | providerScope = config.get('autocompletionFilesScope'); 555 | getPathList(); 556 | registerProviders(); 557 | window.showInformationMessage('HTML to CSS autocompletion: All files have been parsed.'); 558 | isActiveRestoreAllConfigs = false; 559 | } 560 | }; 561 | 562 | // show restore to default confirmation UI 563 | async function askToDefault(configObj) { 564 | const checkInput = await window.showQuickPick([ 'Restore to default', 'Cancel' ]); 565 | if (checkInput === 'Restore to default') configObj.toDefault(); 566 | } 567 | 568 | // update extension configuration within user settings 569 | async function updateConfig(configName, userInput) { 570 | let input; 571 | if ( 572 | configName === 'getSelectorsFromFileTypes' || 573 | configName === 'folderNamesToBeIncluded' || 574 | configName === 'folderNamesToBeExcluded' 575 | ) 576 | input = userInput.split(',').map((elem) => elem.trim()); 577 | else input = userInput.trim(); 578 | await config.update(configName, input); 579 | } 580 | 581 | function removeDisposables(disposables) { 582 | if (disposables) { 583 | disposables.forEach((disposable) => disposable.dispose()); 584 | } else { 585 | watchers.forEach((disposable) => disposable.dispose()); 586 | providers.forEach((disposable) => disposable.dispose()); 587 | } 588 | } 589 | 590 | // will dispose watchers/providers on extension deactivation or VScode exit 591 | function deactivate() { 592 | removeDisposables(); 593 | } 594 | exports.deactivate = deactivate; 595 | --------------------------------------------------------------------------------