├── .editorconfig ├── .gitignore ├── .prettierrc ├── .vscode ├── launch.json ├── settings.json └── tasks.json ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── img ├── codecomplete.gif ├── com.gif ├── comp1.gif ├── def1.gif ├── def2.gif ├── dia.gif └── goto.gif ├── package.json ├── src ├── CompletionProvider.ts ├── DefinitionProvider.ts ├── HoverProvider.ts ├── ReferenceProvider.ts ├── cache │ ├── cache.ts │ └── workSpaceCache.ts ├── css │ └── processCss.ts ├── extension.ts ├── less │ ├── lessImportPlugin.ts │ └── processLess.ts ├── parse │ ├── index.ts │ ├── javascript.ts │ └── typescript.ts ├── typings.ts └── util │ ├── findImportObject.ts │ ├── getLocals.ts │ ├── getWordBeforeDot.ts │ ├── help.ts │ └── vueLanguageRegions.ts ├── test ├── extension.test.ts ├── index.ts └── workspace │ ├── .vscode │ └── settings.json │ ├── node_modules │ └── test │ │ └── index.less │ ├── root.less │ ├── src │ ├── css.css │ ├── function.less │ ├── index.less │ ├── index.ts │ ├── js.js │ ├── usenode.less │ ├── usevariable.less │ ├── variable.less │ └── vue │ │ ├── App.vue │ │ └── out.modules.less │ ├── tsconfig.json │ └── typings │ └── index.d.ts ├── tsconfig.json └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | .vscode-test/ 4 | *.vsix 5 | npm-debug.log 6 | !test/workspace/node_modules -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "printWidth": 120, 5 | "jsxBracketSameLine": true 6 | } 7 | -------------------------------------------------------------------------------- /.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": "Extension", 7 | "type": "extensionHost", 8 | "request": "launch", 9 | "runtimeExecutable": "${execPath}", 10 | "args": ["--extensionDevelopmentPath=${workspaceFolder}" ], 11 | "stopOnEntry": false, 12 | "sourceMaps": true, 13 | "outFiles": [ "${workspaceFolder}/out/**/*.js" ], 14 | "preLaunchTask": "npm: watch" 15 | }, 16 | { 17 | "name": "Extension Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "runtimeExecutable": "${execPath}", 21 | "args": ["--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/test" ], 22 | "stopOnEntry": false, 23 | "sourceMaps": true, 24 | "outFiles": [ "${workspaceFolder}/out/test/**/*.js" ], 25 | "preLaunchTask": "npm: watch" 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 | "perfect-css-modules.rootDir": "/src" 10 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } -------------------------------------------------------------------------------- /.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/** 2 | .vscode-test/** 3 | test/** 4 | src/** 5 | out/test/** 6 | .gitignore 7 | tsconfig.json 8 | vsc-extension-quickstart.md 9 | *.vsix -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [0.3.0] - 2018-04-07 4 | - improve: support modulesPath config. 5 | - fix bug: should not find match classname by prefix 6 | 7 | ## [0.4.0] - 2018-05-02 8 | - improve: referenceprovider 9 | 10 | ## [0.5.0] - 2019-03-17 11 | - improve: support vue sfc 12 | - improve: suport config diagnostic 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # vscode-perfect-css-modules 2 | [![Marketplace Version](https://vsmarketplacebadge.apphb.com/version/wangtao0101.vscode-perfect-css-modules.svg)](https://marketplace.visualstudio.com/items?itemName=wangtao0101.vscode-perfect-css-modules) 3 | [![Installs](https://vsmarketplacebadge.apphb.com/installs/wangtao0101.vscode-perfect-css-modules.svg)](https://marketplace.visualstudio.com/items?itemName=wangtao0101.vscode-perfect-css-modules) 4 | 5 | A vscode extension for css-modules language server. 6 | 7 | # Feature 8 | * autocomplete 9 | * go to definition 10 | * hover tooltip 11 | * provide diagnostic 12 | * support vue scf 13 | 14 | # Snapshot 15 | ## autocomplete 16 | ![GitHub Logo](https://github.com/wangtao0101/vscode-perfect-css-modules/blob/master/img/codecomplete.gif?raw=true) 17 | 18 | ## go to definition 19 | ![GitHub Logo](https://github.com/wangtao0101/vscode-perfect-css-modules/blob/master/img/goto.gif?raw=true) 20 | 21 | ## diagnostic 22 | ![GitHub Logo](https://github.com/wangtao0101/vscode-perfect-css-modules/blob/master/img/dia.gif?raw=true) 23 | 24 | ## vue sfc autocomplete 25 | add module config in style, also support import other style file from local or node_modules 26 | ``` 27 | 38 | ``` 39 | 40 | support autocomplete for $style in template 41 | ![GitHub Logo](https://github.com/wangtao0101/vscode-perfect-css-modules/blob/master/img/com.gif?raw=true) 42 | 43 | support autocomplete for $style in script and support es module style 44 | ![GitHub Logo](https://github.com/wangtao0101/vscode-perfect-css-modules/blob/master/img/comp1.gif?raw=true) 45 | 46 | ## vue sfc go to definition 47 | in vue sfc file 48 | ![GitHub Logo](https://github.com/wangtao0101/vscode-perfect-css-modules/blob/master/img/def1.gif?raw=true) 49 | 50 | goto style file 51 | ![GitHub Logo](https://github.com/wangtao0101/vscode-perfect-css-modules/blob/master/img/def2.gif?raw=true) 52 | 53 | ## how to config in vue project 54 | 1. enable css-modules in vue-cli 55 | 2. enable camelCase in vue.config.js 56 | ``` 57 | module.exports = { 58 | css: { 59 | sourceMap: true, 60 | loaderOptions: { 61 | css: { 62 | camelCase: true, 63 | } 64 | } 65 | }, 66 | } 67 | ``` 68 | 69 | ## how to config in react project 70 | 1. enable css-modules in css-loader 71 | 2. enable camelCase namedExport in css-loader 72 | ``` 73 | { 74 | loader: require.resolve('css-loader'), 75 | options: { 76 | modules: true, 77 | namedExport: true, 78 | camelCase: true, 79 | }, 80 | } 81 | ``` 82 | 83 | # Imports 84 | The behavior is the same as [less loader webpack resolver](https://github.com/webpack-contrib/less-loader#imports). 85 | 86 | You can import your Less modules from `node_modules`. Just prepend them with a `~` which tells extension to look up the [`modules`]. 87 | 88 | ```less 89 | @import "~bootstrap/less/bootstrap"; 90 | ``` 91 | 92 | # Config 93 | ## perfect-css-modules.rootDir 94 | Specifies the root directory of input files relative to project workspace, including js, ts, css, less. Defaults to ., you can set /src. 95 | 96 | ## perfect-css-modules.camelCase 97 | Export Classnames in camelOnly or dashesOnly. 98 | 99 | ## perfect-css-modules.styleFilesToScan 100 | Glob for files to watch and scan. Defaults to **/*.{less,css}. 101 | 102 | ## perfect-css-modules.jsFilesToScan 103 | Glob for files to watch and scan. Defaults to **/*.{js,ts,jsx,tsx} 104 | 105 | ## perfect-css-modules.modulesPath 106 | Specifies the node_modules directory. Defaults to ./node_modules. See [Imports](https://github.com/wangtao0101/vscode-perfect-css-modules#imports). 107 | 108 | ## perfect-css-modules.enableDiagnostic 109 | enable diagnostic, Defaults to true 110 | 111 | # TODO 112 | - [x] support js 113 | - [x] support ts 114 | - [x] support less 115 | - [x] support css 116 | - [x] support vue 117 | - [ ] support sass 118 | - [ ] support Custom Inject Name in vue sfc 119 | -------------------------------------------------------------------------------- /img/codecomplete.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangtao0101/vscode-perfect-css-modules/7b3f189af7dd1f0398adefbb69fa93eddb5f0a1a/img/codecomplete.gif -------------------------------------------------------------------------------- /img/com.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangtao0101/vscode-perfect-css-modules/7b3f189af7dd1f0398adefbb69fa93eddb5f0a1a/img/com.gif -------------------------------------------------------------------------------- /img/comp1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangtao0101/vscode-perfect-css-modules/7b3f189af7dd1f0398adefbb69fa93eddb5f0a1a/img/comp1.gif -------------------------------------------------------------------------------- /img/def1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangtao0101/vscode-perfect-css-modules/7b3f189af7dd1f0398adefbb69fa93eddb5f0a1a/img/def1.gif -------------------------------------------------------------------------------- /img/def2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangtao0101/vscode-perfect-css-modules/7b3f189af7dd1f0398adefbb69fa93eddb5f0a1a/img/def2.gif -------------------------------------------------------------------------------- /img/dia.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangtao0101/vscode-perfect-css-modules/7b3f189af7dd1f0398adefbb69fa93eddb5f0a1a/img/dia.gif -------------------------------------------------------------------------------- /img/goto.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wangtao0101/vscode-perfect-css-modules/7b3f189af7dd1f0398adefbb69fa93eddb5f0a1a/img/goto.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vscode-perfect-css-modules", 3 | "displayName": "perfect-css-modules", 4 | "description": "", 5 | "version": "0.5.0", 6 | "publisher": "wangtao0101", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/wangtao0101/vscode-perfect-css-modules.git" 10 | }, 11 | "categories": [ 12 | "Other" 13 | ], 14 | "engines": { 15 | "vscode": "^1.21.0" 16 | }, 17 | "activationEvents": [ 18 | "onLanguage:javascript", 19 | "onLanguage:javascriptreact", 20 | "onLanguage:typescript", 21 | "onLanguage:typescriptreact", 22 | "onLanguage:css", 23 | "onLanguage:less", 24 | "onLanguage:scss", 25 | "onLanguage:vue" 26 | ], 27 | "main": "./out/src/extension", 28 | "contributes": { 29 | "configuration": { 30 | "type": "object", 31 | "title": "js import configuration", 32 | "properties": { 33 | "perfect-css-modules.rootDir": { 34 | "type": "string", 35 | "default": ".", 36 | "description": "Specifies the root directory of input files relative to project workspace, including js, ts, css, less. Defaults to ., you can set /src", 37 | "scope": "resource" 38 | }, 39 | "perfect-css-modules.camelCase": { 40 | "type": "string", 41 | "default": "camelOnly", 42 | "description": "Export Classnames in camelOnly or dashesOnly", 43 | "scope": "resource" 44 | }, 45 | "perfect-css-modules.styleFilesToScan": { 46 | "type": "string", 47 | "default": "**/*.{less,css}", 48 | "description": "Glob for files to watch and scan. Defaults to **/*.{less,css}.", 49 | "scope": "resource" 50 | }, 51 | "perfect-css-modules.jsFilesToScan": { 52 | "type": "string", 53 | "default": "**/*.{js,ts,jsx,tsx}", 54 | "description": "Glob for files to watch and scan. Defaults to **/*.{js,ts,jsx,tsx}", 55 | "scope": "resource" 56 | }, 57 | "perfect-css-modules.modulesPath": { 58 | "type": "string", 59 | "default": "./node_modules", 60 | "description": "Specifies the node_modules directory, see https://github.com/wangtao0101/vscode-perfect-css-modules#imports", 61 | "scope": "resource" 62 | }, 63 | "perfect-css-modules.enableDiagnostic": { 64 | "type": "boolean", 65 | "default": "true", 66 | "description": "enable diagnostic, Defaults to true", 67 | "scope": "resource" 68 | } 69 | } 70 | } 71 | }, 72 | "scripts": { 73 | "vscode:prepublish": "npm run compile", 74 | "compile": "tsc -p ./", 75 | "watch": "tsc -watch -p ./", 76 | "postinstall": "node ./node_modules/vscode/bin/install", 77 | "test": "npm run compile && node ./node_modules/vscode/bin/test" 78 | }, 79 | "devDependencies": { 80 | "@types/mocha": "^2.2.42", 81 | "@types/node": "^9.4.7", 82 | "vscode": "^1.1.13" 83 | }, 84 | "dependencies": { 85 | "@babel/traverse": "^7.0.0-beta.44", 86 | "@babel/types": "^7.0.0-beta.44", 87 | "css-loader": "^0.28.11", 88 | "less": "^3.0.1", 89 | "parse-import-es6": "^0.5.9", 90 | "pify": "^3.0.0", 91 | "typescript": "^3.3.3333", 92 | "vfile": "^2.3.0", 93 | "vfile-location": "^2.0.2", 94 | "vue-language-server": "^0.0.45" 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/CompletionProvider.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import * as vscode from 'vscode'; 3 | import Cache from './cache/cache'; 4 | import { StyleObject } from './typings'; 5 | import { findMatchModuleSpecifier } from './util/findImportObject'; 6 | import getWordBeforeDot from './util/getWordBeforeDot'; 7 | import getVueLanguageRegions from './util/vueLanguageRegions'; 8 | import processLess from './less/processLess'; 9 | import { getStringAttr } from './util/help'; 10 | 11 | export default class CompletionProvider implements vscode.CompletionItemProvider { 12 | public async provideCompletionItems( 13 | document: vscode.TextDocument, 14 | position: vscode.Position, 15 | token: vscode.CancellationToken, 16 | ): Promise { 17 | let identifier = null; 18 | // let wordToComplete = ''; 19 | const range = document.getWordRangeAtPosition(position); 20 | if (range) { 21 | // wordToComplete = document.getText(new vscode.Range(range.start, range.end)); 22 | // the range should be after dot, so we find dot. 23 | identifier = getWordBeforeDot(document, range.start); 24 | } else { 25 | // there is no word under cursor, so we check whether the preview word is dot. 26 | identifier = getWordBeforeDot(document, position); 27 | } 28 | 29 | if (identifier == null) { 30 | return []; 31 | } 32 | 33 | if (document.languageId === 'vue' && identifier === '$style') { 34 | // TODO: Custom Inject Name 35 | return await this.provideVueCompletionItems(identifier, document, position); 36 | } 37 | 38 | const moduleSpecifier = findMatchModuleSpecifier(document.getText(), identifier); 39 | 40 | if (moduleSpecifier == null) { 41 | return []; 42 | } 43 | 44 | const uri = path.join(path.dirname(document.fileName), moduleSpecifier); 45 | const style: StyleObject = Cache.getStyleObject(vscode.Uri.file(uri)); 46 | 47 | if (style != null) { 48 | const locals = style.locals; 49 | return this.generateItemFromLocal(locals); 50 | } 51 | return []; 52 | } 53 | 54 | private async provideVueCompletionItems( 55 | identifier: string, 56 | document: vscode.TextDocument, 57 | position: vscode.Position, 58 | ): Promise { 59 | const regions = getVueLanguageRegions(document.getText()).getAllStyleRegions(); 60 | const items = []; 61 | for (const region of regions) { 62 | const start = new vscode.Position(region.start.line, region.start.character); 63 | const end = new vscode.Position(region.end.line, region.end.character); 64 | const sourceRange = new vscode.Range(start, end); 65 | const workspace = vscode.workspace.getWorkspaceFolder(document.uri); 66 | const result = await processLess( 67 | document.getText(sourceRange), 68 | workspace.uri.fsPath, 69 | document.uri.fsPath, 70 | true, 71 | getStringAttr('modulesPath', document.uri), 72 | ); 73 | if (result) { 74 | items.push(...this.generateItemFromLocal(result.locals)); 75 | } 76 | } 77 | return items; 78 | } 79 | 80 | private generateItemFromLocal(locals) { 81 | const items = []; 82 | Object.keys(locals).map(key => { 83 | items.push({ 84 | label: key, 85 | kind: vscode.CompletionItemKind.Reference, 86 | detail: key, 87 | documentation: locals[key].name, 88 | }); 89 | }); 90 | return items; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/DefinitionProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import getWordBeforeDot from './util/getWordBeforeDot'; 5 | import { findMatchModuleSpecifier } from './util/findImportObject'; 6 | import processLess from './less/processLess'; 7 | import Cache from './cache/cache'; 8 | import { StyleObject, Local } from './typings'; 9 | import getVueLanguageRegions from './util/vueLanguageRegions'; 10 | import { getStringAttr } from './util/help'; 11 | 12 | export default class CSSModuleDefinitionProvider implements vscode.DefinitionProvider { 13 | public async provideDefinition( 14 | document: vscode.TextDocument, 15 | position: vscode.Position, 16 | token: vscode.CancellationToken, 17 | ): Promise { 18 | const range = document.getWordRangeAtPosition(position); 19 | if (range == null) { 20 | return null; 21 | } 22 | const wordToDefinition = document.getText(new vscode.Range(range.start, range.end)); 23 | const identifier = getWordBeforeDot(document, range.start); 24 | 25 | if (identifier == null) { 26 | // just a word 27 | } else { 28 | // find xxx.abc 29 | 30 | if (document.languageId === 'vue' && identifier === '$style') { 31 | return await this.provideVueDefinition(wordToDefinition, document, position); 32 | } 33 | 34 | const moduleSpecifier = findMatchModuleSpecifier(document.getText(), identifier); 35 | 36 | if (moduleSpecifier == null) { 37 | return []; 38 | } 39 | 40 | const uri = path.join(path.dirname(document.fileName), moduleSpecifier); 41 | 42 | const style: StyleObject = Cache.getStyleObject(vscode.Uri.file(uri)); 43 | 44 | if (style != null) { 45 | const locals = style.locals; 46 | let matchLocal: Local = null; 47 | Object.keys(locals).map(key => { 48 | if (key === wordToDefinition) { 49 | matchLocal = locals[key]; 50 | } 51 | }); 52 | if (matchLocal != null) { 53 | const position = []; 54 | let start; 55 | let end; 56 | if (matchLocal.positions.length === 0) { 57 | start = new vscode.Position(0, 0); 58 | end = new vscode.Position(0, 1); 59 | position.push(new vscode.Location(vscode.Uri.file(uri), new vscode.Range(start, end))); 60 | } else { 61 | matchLocal.positions.map(po => { 62 | // TODO: check the word in file for $ if less file 63 | start = new vscode.Position(po.line, po.column); 64 | end = new vscode.Position(po.line, po.column + matchLocal.name.length); 65 | position.push(new vscode.Location(vscode.Uri.file(po.fsPath), new vscode.Range(start, end))); 66 | }); 67 | } 68 | return position; 69 | } 70 | } 71 | return null; 72 | } 73 | } 74 | 75 | private async provideVueDefinition( 76 | word: string, 77 | document: vscode.TextDocument, 78 | position: vscode.Position, 79 | ): Promise { 80 | const regions = getVueLanguageRegions(document.getText()).getAllStyleRegions(); 81 | const items = []; 82 | for (const region of regions) { 83 | const start = new vscode.Position(region.start.line, region.start.character); 84 | const end = new vscode.Position(region.end.line, region.end.character); 85 | const sourceRange = new vscode.Range(start, end); 86 | const workspace = vscode.workspace.getWorkspaceFolder(document.uri); 87 | const result = await processLess( 88 | document.getText(sourceRange), 89 | workspace.uri.fsPath, 90 | document.uri.fsPath, 91 | true, 92 | getStringAttr('modulesPath', document.uri), 93 | ); 94 | if (result) { 95 | items.push(...this.generateItemFromLocal(result.locals, word, document.uri, region.start.line)); 96 | } 97 | } 98 | return items; 99 | } 100 | 101 | private generateItemFromLocal(locals, word, uri, lineGap) { 102 | const items = []; 103 | Object.keys(locals).map(key => { 104 | if (key === word) { 105 | const matchLocal = locals[key]; 106 | let start; 107 | let end; 108 | if (matchLocal.positions.length === 0) { 109 | start = new vscode.Position(0, 0); 110 | end = new vscode.Position(0, 1); 111 | items.push(new vscode.Location(vscode.Uri.file(uri), new vscode.Range(start, end))); 112 | } else { 113 | matchLocal.positions.map(po => { 114 | const line = po.fsPath.endsWith('vue') ? po.line + lineGap : po.line; 115 | // TODO: check the word in file for $ if less file 116 | start = new vscode.Position(line, po.column); 117 | end = new vscode.Position(line, po.column + matchLocal.name.length); 118 | items.push(new vscode.Location(vscode.Uri.file(po.fsPath), new vscode.Range(start, end))); 119 | }); 120 | } 121 | } 122 | }); 123 | return items; 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/HoverProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | import getWordBeforeDot from './util/getWordBeforeDot'; 5 | import { findMatchModuleSpecifier } from './util/findImportObject'; 6 | import processLess from './less/processLess'; 7 | import { StyleObject, Local } from './typings'; 8 | import Cache from './cache/cache'; 9 | 10 | export default class CSSModuleHoverProvider implements vscode.HoverProvider { 11 | public async provideHover( 12 | document: vscode.TextDocument, 13 | position: vscode.Position, 14 | token: vscode.CancellationToken, 15 | ): Promise { 16 | const range = document.getWordRangeAtPosition(position); 17 | if (range == null) { 18 | return null; 19 | } 20 | const wordToDefinition = document.getText(new vscode.Range(range.start, range.end)); 21 | const identifier = getWordBeforeDot(document, range.start); 22 | 23 | if (identifier == null) { 24 | // just a word 25 | } else { 26 | // find xxx.abc 27 | const moduleSpecifier = findMatchModuleSpecifier(document.getText(), identifier); 28 | 29 | if (moduleSpecifier == null) { 30 | return null; 31 | } 32 | 33 | const uri = path.join(path.dirname(document.fileName), moduleSpecifier); 34 | const style: StyleObject = Cache.getStyleObject(vscode.Uri.file(uri)); 35 | 36 | if (style != null) { 37 | const locals = style.locals; 38 | let matchLocal: Local = null; 39 | Object.keys(locals).map(key => { 40 | if (key === wordToDefinition) { 41 | matchLocal = locals[key]; 42 | } 43 | }); 44 | if (matchLocal != null) { 45 | return new vscode.Hover(matchLocal.name); 46 | } 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/ReferenceProvider.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import * as fs from 'fs'; 3 | import * as pify from 'pify'; 4 | import { StyleObject, PropertyAccessExpression, StyleImport } from './typings'; 5 | import Cache from './cache/cache'; 6 | import { compile } from './parse'; 7 | 8 | const readFile = pify(fs.readFile.bind(fs)); 9 | const vfile = require('vfile'); 10 | const vfileLocation = require('vfile-location'); 11 | 12 | export default class ReferenceProvider implements vscode.ReferenceProvider { 13 | public async provideReferences( 14 | document: vscode.TextDocument, 15 | position: vscode.Position, 16 | context: vscode.ReferenceContext, 17 | token: vscode.CancellationToken, 18 | ): Promise { 19 | // TODO: maybe we should find word use sourcemap for less or sass 20 | const range = document.getWordRangeAtPosition(position); 21 | if (range) { 22 | let word = document.getText(new vscode.Range(range.start, range.end)); 23 | if (word.startsWith('.')) { 24 | word = word.substring(1); 25 | } 26 | const style: StyleObject = Cache.getStyleObject(document.uri); 27 | if (style == null) { 28 | return []; 29 | } 30 | const name = Object.keys(style.locals).find(local => style.locals[local].name === word); 31 | if (name == null) { 32 | return []; 33 | } 34 | const styleImports: StyleImport[] = Cache.getStyleImportByStyleFile(document.uri); 35 | const result: vscode.Location[] = []; 36 | for (const styleImport of styleImports) { 37 | const data = await readFile(styleImport.jsFsPath, 'utf8'); 38 | const paes: PropertyAccessExpression[] = compile(data, styleImport.jsFsPath, [styleImport]); 39 | const location = vfileLocation(vfile(data)); 40 | for (const pae of paes) { 41 | if (pae.right === name) { 42 | const rangeStart = location.toPosition(pae.pos); // offset: 0-based 43 | const rangeEnd = location.toPosition(pae.end); // offset: 0-based 44 | const vsRange = new vscode.Range( 45 | rangeStart.line - 1, 46 | rangeStart.column - 1, 47 | rangeEnd.line - 1, 48 | rangeEnd.column - 1, 49 | ); 50 | result.push(new vscode.Location(vscode.Uri.file(pae.styleImport.jsFsPath), vsRange)); 51 | } 52 | } 53 | } 54 | return result; 55 | } 56 | return []; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/cache/cache.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import WorkSpaceCache from './workSpaceCache'; 3 | import { StyleObject, StyleImport } from '../typings'; 4 | 5 | export default class Cache { 6 | private static cache = {}; 7 | 8 | public static buildCache (diagnosticCollection) { 9 | if (vscode.workspace.workspaceFolders) { 10 | vscode.workspace.workspaceFolders.map(item => { 11 | Cache.cache[item.uri.fsPath] = new WorkSpaceCache(item, diagnosticCollection) 12 | }) 13 | } 14 | } 15 | 16 | public static buildWorkSpaceCache(item: vscode.WorkspaceFolder, diagnosticCollection) { 17 | Cache.deleteWorkSpaceCache(item); 18 | Cache.cache[item.uri.fsPath] = new WorkSpaceCache(item, diagnosticCollection); 19 | } 20 | 21 | public static deleteWorkSpaceCache(item: vscode.WorkspaceFolder) { 22 | if (Cache.cache[item.uri.fsPath] != null) { 23 | Cache.cache[item.uri.fsPath].dispose(); 24 | delete Cache.cache[item.uri.fsPath]; 25 | } 26 | } 27 | 28 | /** 29 | * get parsed style file objcet 30 | * @param uri file URI 31 | */ 32 | public static getStyleObject(uri: vscode.Uri) { 33 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); 34 | const wsc: WorkSpaceCache = Cache.cache[workspaceFolder.uri.fsPath]; 35 | if (wsc == null) { 36 | return null; 37 | } 38 | return wsc.getStyleObject(uri.fsPath); 39 | } 40 | 41 | public static getStyleImportByStyleFile(uri: vscode.Uri) : StyleImport[] { 42 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); 43 | const wsc: WorkSpaceCache = Cache.cache[workspaceFolder.uri.fsPath]; 44 | if (wsc == null) { 45 | return null; 46 | } 47 | return wsc.getStyleImportByStyleFile(uri.fsPath); 48 | } 49 | 50 | public static getWorkSpaceCache(uri: vscode.Uri): StyleObject { 51 | const workspaceFolder = vscode.workspace.getWorkspaceFolder(uri); 52 | return Cache.cache[workspaceFolder.uri.fsPath]; 53 | } 54 | } -------------------------------------------------------------------------------- /src/cache/workSpaceCache.ts: -------------------------------------------------------------------------------- 1 | import { WorkspaceFolder, RelativePattern, FileSystemWatcher } from 'vscode'; 2 | import * as vscode from 'vscode'; 3 | import * as fs from 'fs'; 4 | import * as path from 'path'; 5 | import * as pify from 'pify'; 6 | import processLess from '../less/processLess'; 7 | import processCss from '../css/processCss'; 8 | import { StyleImport, StyleObject } from '../typings'; 9 | import { findAllStyleImports } from '../util/findImportObject'; 10 | import { compile } from '../parse'; 11 | import { TSTypeAliasDeclaration } from '@babel/types'; 12 | 13 | const vfile = require('vfile'); 14 | const vfileLocation = require('vfile-location'); 15 | 16 | const readFile = pify(fs.readFile.bind(fs)); 17 | 18 | const isLess = /\.less$/; 19 | const isCss = /\.css$/; 20 | 21 | export default class WorkSpaceCache { 22 | private fileWatcher: Array = []; 23 | private workspaceFolder: WorkspaceFolder; 24 | private styleCache = {}; 25 | private styleImportsCache: Map = new Map(); 26 | private camelCase; 27 | private styleFilesToScan; 28 | private jsFilesToScan; 29 | private rootDir; 30 | private diagnosticCollection: vscode.DiagnosticCollection; 31 | private modulePath; 32 | private enableDiagnostic; 33 | 34 | constructor(workspaceFolder: WorkspaceFolder, diagnosticCollection: vscode.DiagnosticCollection) { 35 | this.workspaceFolder = workspaceFolder; 36 | this.diagnosticCollection = diagnosticCollection; 37 | 38 | this.rootDir = vscode.workspace 39 | .getConfiguration('perfect-css-modules', this.workspaceFolder.uri) 40 | .get('rootDir'); 41 | this.camelCase = vscode.workspace 42 | .getConfiguration('perfect-css-modules', this.workspaceFolder.uri) 43 | .get('camelCase'); 44 | this.styleFilesToScan = vscode.workspace 45 | .getConfiguration('perfect-css-modules', this.workspaceFolder.uri) 46 | .get('styleFilesToScan'); 47 | this.jsFilesToScan = vscode.workspace 48 | .getConfiguration('perfect-css-modules', this.workspaceFolder.uri) 49 | .get('jsFilesToScan'); 50 | this.modulePath = vscode.workspace 51 | .getConfiguration('perfect-css-modules', this.workspaceFolder.uri) 52 | .get('modulesPath'); 53 | this.enableDiagnostic = vscode.workspace 54 | .getConfiguration('perfect-css-modules', this.workspaceFolder.uri) 55 | .get('enableDiagnostic'); 56 | 57 | this.init(); 58 | } 59 | 60 | private async init() { 61 | await this.processAllStyleFiles(); 62 | this.processAllJsFiles(); 63 | this.addFileWatcher(); 64 | } 65 | 66 | private addFileWatcher() { 67 | const relativePattern = new RelativePattern( 68 | path.join(this.workspaceFolder.uri.fsPath, this.rootDir), 69 | this.styleFilesToScan, 70 | ); 71 | const styleWatcher = vscode.workspace.createFileSystemWatcher(relativePattern); 72 | styleWatcher.onDidChange((file: vscode.Uri) => { 73 | this.processStyleFile(file); 74 | this.regenerateDiagnostic(file.fsPath); 75 | }); 76 | styleWatcher.onDidCreate((file: vscode.Uri) => { 77 | this.processStyleFile(file); 78 | this.regenerateDiagnostic(file.fsPath); 79 | }); 80 | styleWatcher.onDidDelete((file: vscode.Uri) => { 81 | delete this.styleCache[file.fsPath]; 82 | }); 83 | 84 | const relativePatternJs = new RelativePattern( 85 | path.join(this.workspaceFolder.uri.fsPath, this.rootDir), 86 | this.jsFilesToScan, 87 | ); 88 | const jsWatcher = vscode.workspace.createFileSystemWatcher(relativePatternJs); 89 | jsWatcher.onDidChange((file: vscode.Uri) => { 90 | this.processJsFile(file); 91 | }); 92 | jsWatcher.onDidCreate((file: vscode.Uri) => { 93 | this.processJsFile(file); 94 | }); 95 | jsWatcher.onDidDelete((file: vscode.Uri) => { 96 | delete this.styleImportsCache[file.fsPath]; 97 | if (this.enableDiagnostic) { 98 | this.diagnosticCollection.delete(file); 99 | } 100 | }); 101 | this.fileWatcher.push(styleWatcher); 102 | this.fileWatcher.push(jsWatcher); 103 | } 104 | 105 | private async processAllStyleFiles() { 106 | const relativePattern = new RelativePattern( 107 | path.join(this.workspaceFolder.uri.fsPath, this.rootDir), 108 | this.styleFilesToScan, 109 | ); 110 | const files = await vscode.workspace.findFiles(relativePattern, '{**/node_modules/**}', 99999); 111 | for (const file of files) { 112 | await this.processStyleFile(file); 113 | } 114 | } 115 | 116 | /** 117 | * regenerate Diagnostic after change style file 118 | * @param styleFilePath 119 | */ 120 | private regenerateDiagnostic(styleFilePath: string) { 121 | if (!this.enableDiagnostic) { 122 | return; 123 | } 124 | const relatedJsFilePaths = []; 125 | Object.keys(this.styleImportsCache).map(jsPath => { 126 | const sis: StyleImport[] = this.styleImportsCache[jsPath]; 127 | sis.map(si => { 128 | if (si.styleFsPath === styleFilePath) { 129 | relatedJsFilePaths.push(si.jsFsPath); 130 | } 131 | }); 132 | }); 133 | relatedJsFilePaths.map(jsPath => { 134 | this.processJsFile(vscode.Uri.file(jsPath)); 135 | }); 136 | } 137 | 138 | private async processStyleFile(file: vscode.Uri) { 139 | try { 140 | const data = await readFile(file.fsPath, 'utf8'); 141 | if (file.fsPath.match(isLess)) { 142 | const result = await processLess( 143 | data, 144 | this.workspaceFolder.uri.fsPath, 145 | file.fsPath, 146 | this.camelCase, 147 | this.modulePath, 148 | ); 149 | this.styleCache[file.fsPath] = result; 150 | } else if (file.fsPath.match(isCss)) { 151 | const result = await processCss(data, file.fsPath, this.camelCase); 152 | this.styleCache[file.fsPath] = result; 153 | } 154 | } catch (error) { 155 | console.log(error); 156 | } 157 | } 158 | 159 | private async processAllJsFiles() { 160 | const relativePattern = new RelativePattern( 161 | path.join(this.workspaceFolder.uri.fsPath, this.rootDir), 162 | this.jsFilesToScan, 163 | ); 164 | const files = await vscode.workspace.findFiles(relativePattern, '{**/node_modules/**}', 99999); 165 | files.forEach(file => { 166 | this.processJsFile(file); 167 | }); 168 | } 169 | 170 | private async processJsFile(file: vscode.Uri) { 171 | try { 172 | const data = await readFile(file.fsPath, 'utf8'); 173 | const styleImports = findAllStyleImports(data, file.fsPath); 174 | this.styleImportsCache[file.fsPath] = styleImports; 175 | 176 | if (this.enableDiagnostic) { 177 | this.diagnosticCollection.delete(file); 178 | if (styleImports.length !== 0) { 179 | const diags: Array = []; 180 | const paes = compile(data, file.fsPath, styleImports); 181 | const location = vfileLocation(vfile(data)); 182 | for (const pae of paes) { 183 | const styleObject = await this.getStyleAsync(pae.styleImport.styleFsPath); 184 | if (styleObject != null && styleObject.locals[pae.right] == null) { 185 | /** 186 | * range { 187 | * line: 1-based 188 | * column: 1-based 189 | * } 190 | */ 191 | const rangeStart = location.toPosition(pae.pos); // offset: 0-based 192 | const rangeEnd = location.toPosition(pae.end); // offset: 0-based 193 | const vsRange = new vscode.Range( 194 | rangeStart.line - 1, 195 | rangeStart.column - 1, 196 | rangeEnd.line - 1, 197 | rangeEnd.column - 1, 198 | ); 199 | diags.push( 200 | new vscode.Diagnostic( 201 | vsRange, 202 | `perfect-css-module: Cannot find ${pae.right} in ${pae.left}.`, 203 | vscode.DiagnosticSeverity.Error, 204 | ), 205 | ); 206 | } 207 | } 208 | if (diags.length !== 0) { 209 | this.diagnosticCollection.set(file, diags); 210 | } 211 | } 212 | } 213 | } catch (error) { 214 | console.log(error); 215 | } 216 | } 217 | 218 | private async getStyleAsync(fsPath: string): Promise { 219 | let styleObject = this.getStyleObject(fsPath); 220 | if (styleObject != null) { 221 | return styleObject; 222 | } 223 | // attemp to read file and getStyleObject again 224 | await this.processStyleFile(vscode.Uri.file(fsPath)); 225 | return this.getStyleObject(fsPath); 226 | } 227 | 228 | public getStyleObject(fsPath: string): StyleObject { 229 | return this.styleCache[fsPath]; 230 | } 231 | 232 | public getStyleImportByStyleFile(fsPath: string): StyleImport[] { 233 | const styleImports: StyleImport[] = []; 234 | Object.keys(this.styleImportsCache).filter(key => { 235 | const styleImport: Array = this.styleImportsCache[key]; 236 | styleImport.map(si => { 237 | if (si.styleFsPath === fsPath) { 238 | styleImports.push(si); 239 | } 240 | }); 241 | }); 242 | return styleImports; 243 | } 244 | 245 | public dispose() { 246 | this.fileWatcher.map(fw => { 247 | fw.dispose(); 248 | }); 249 | } 250 | } 251 | -------------------------------------------------------------------------------- /src/css/processCss.ts: -------------------------------------------------------------------------------- 1 | import getLocals from "../util/getLocals"; 2 | import { Position } from "../typings"; 3 | 4 | const vfile = require('vfile'); 5 | const vfileLocation = require('vfile-location'); 6 | 7 | function getOriginalPositions(className, css: string = '', cssLocation, filePath): Array { 8 | const positions: Array = []; 9 | let offset = 0; 10 | while (true) { 11 | // TODO: find exact match word, do not use index of 12 | offset = css.indexOf(`.${className}`, offset); 13 | if (offset === -1) { 14 | break; 15 | } 16 | 17 | const tmpchar = css[offset + className.length + 1]; 18 | if(/[a-zA-Z]/.test(tmpchar)) { 19 | offset += 1; 20 | continue; 21 | } 22 | /** 23 | * range { 24 | * line: 1-based 25 | * column: 1-based 26 | * } 27 | */ 28 | const range = cssLocation.toPosition(offset); // offset: 0-based 29 | positions.push({ 30 | line: range.line - 1, // 0-based 31 | column: range.column - 1, // 0-based 32 | fsPath: filePath, 33 | }) 34 | 35 | offset += 1; 36 | } 37 | 38 | return positions; 39 | } 40 | 41 | async function addPositoinForLocals(localKeys, css, filePath) { 42 | const locals = {}; 43 | const location = vfileLocation(vfile(css)); 44 | Object.keys(localKeys).map(key => { 45 | const positions = getOriginalPositions(localKeys[key], css, location, filePath); 46 | locals[key] = { 47 | name: localKeys[key], 48 | positions, 49 | } 50 | }) 51 | return locals; 52 | } 53 | 54 | export default async function processCss(source, filePath, camelCase) { 55 | try { 56 | const localKeys = await getLocals(source, camelCase); 57 | const locals = await addPositoinForLocals(localKeys, source, filePath); 58 | return { 59 | locals, 60 | css: source, 61 | source, 62 | } 63 | } catch (error) { 64 | console.log(error); 65 | } 66 | } -------------------------------------------------------------------------------- /src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | import CompletionProvider from './CompletionProvider'; 3 | import CSSModuleDefinitionProvider from './DefinitionProvider'; 4 | import CSSModuleHoverProvider from './HoverProvider'; 5 | import Cache from './cache/cache'; 6 | import ReferenceProvider from './ReferenceProvider'; 7 | 8 | export function activate(context: vscode.ExtensionContext) { 9 | console.log('perfect-css-moules extension is now active!'); 10 | 11 | const mode: vscode.DocumentFilter[] = [ 12 | { language: "javascript", scheme: "file" }, 13 | { language: "javascriptreact", scheme: "file" }, 14 | { language: "typescript", scheme: "file" }, 15 | { language: "typescriptreact", scheme: "file" }, 16 | { language: "vue", scheme: "file" }, 17 | ]; 18 | 19 | const stylemode: vscode.DocumentFilter[] = [ 20 | { language: "css", scheme: "file" }, 21 | { language: "less", scheme: "file" }, 22 | ]; 23 | 24 | const completetion = vscode.languages.registerCompletionItemProvider(mode, new CompletionProvider(), '.'); 25 | const definition = vscode.languages.registerDefinitionProvider(mode, new CSSModuleDefinitionProvider()); 26 | const hover = vscode.languages.registerHoverProvider(mode, new CSSModuleHoverProvider()); 27 | const reference = vscode.languages.registerReferenceProvider(stylemode, new ReferenceProvider()); 28 | const diagnosticCollection = vscode.languages.createDiagnosticCollection('perfect-css-modules'); 29 | 30 | Cache.buildCache(diagnosticCollection); 31 | 32 | const wfWatcher = vscode.workspace.onDidChangeWorkspaceFolders((event) => { 33 | event.added.map(item => { 34 | Cache.buildWorkSpaceCache(item, diagnosticCollection) 35 | }) 36 | event.removed.map(item => { 37 | Cache.deleteWorkSpaceCache(item) 38 | }); 39 | }) 40 | 41 | context.subscriptions.push(diagnosticCollection, wfWatcher, completetion, definition, hover); 42 | } 43 | 44 | export function deactivate() { 45 | } 46 | -------------------------------------------------------------------------------- /src/less/lessImportPlugin.ts: -------------------------------------------------------------------------------- 1 | const less = require('less'); 2 | import * as path from 'path'; 3 | import * as fs from 'fs'; 4 | 5 | const isModuleName = /^~[^/\\]+$/; 6 | 7 | export default function LessImportPlugin(modulePath: string) { 8 | 9 | class ImportFileManager extends less.FileManager { 10 | supports() { 11 | return true; 12 | } 13 | 14 | supportsSync() { 15 | return false; 16 | } 17 | 18 | loadFile(filename: string, currentDirectory, options) { 19 | let url: string; 20 | const isNpm = filename.startsWith('~'); 21 | if (options.ext && !isModuleName.test(filename)) { 22 | url = this.tryAppendExtension(filename, options.ext); 23 | } else { 24 | url = filename; 25 | } 26 | 27 | let name; 28 | if (isNpm) { 29 | name = path.join(options.rootpath, modulePath, url.substr(1)); 30 | } else { 31 | name = path.join(currentDirectory, url); 32 | } 33 | 34 | if (fs.existsSync(name)) { 35 | const data = fs.readFileSync(name, 'utf-8'); 36 | return Promise.resolve({ 37 | filename: name, 38 | contents: data, 39 | }); 40 | } 41 | return Promise.reject({ 42 | type: 'File', 43 | message: "'" + filename + "' wasn't found. " 44 | }); 45 | } 46 | } 47 | 48 | return { 49 | install(lessInstance, pluginManager) { 50 | pluginManager.addFileManager(new ImportFileManager()); 51 | }, 52 | minVersion: [2, 1, 1], 53 | }; 54 | } -------------------------------------------------------------------------------- /src/less/processLess.ts: -------------------------------------------------------------------------------- 1 | const less = require('less'); 2 | import { SourceMapConsumer } from 'source-map'; 3 | import * as path from 'path'; 4 | import getLocals from '../util/getLocals'; 5 | import LessImportPlugin from './lessImportPlugin'; 6 | import { StyleObject, Position } from '../typings'; 7 | const vfile = require('vfile'); 8 | const vfileLocation = require('vfile-location'); 9 | 10 | function getOriginalPositions(sourceMapConsumer, className: string, css: string = '', cssLocation): Array { 11 | const positions: Array = []; 12 | let offset = 0; 13 | while (true) { 14 | // TODO: find exact match word, do not use index of 15 | offset = css.indexOf(`.${className}`, offset); 16 | if (offset === -1) { 17 | break; 18 | } 19 | 20 | const tmpchar = css[offset + className.length + 1]; 21 | if(/[a-zA-Z]/.test(tmpchar)) { 22 | offset += 1; 23 | continue; 24 | } 25 | /** 26 | * range { 27 | * line: 1-based 28 | * column: 1-based 29 | * } 30 | */ 31 | const range = cssLocation.toPosition(offset); // offset: 0-based 32 | /** 33 | * sourceRange { 34 | * line: 1-based 35 | * column: 0-based 36 | * } 37 | */ 38 | const sourceRange = sourceMapConsumer.originalPositionFor({ 39 | line: range.line, // line: 1-based 40 | column: range.column - 1, // column: 0-based 41 | }); 42 | if (sourceRange.line != null) { 43 | positions.push({ 44 | line: sourceRange.line - 1, // 0-based 45 | column: sourceRange.column, // 0-based 46 | fsPath: sourceRange.source, 47 | }) 48 | } 49 | 50 | offset += 1; 51 | } 52 | 53 | return positions; 54 | } 55 | 56 | async function addPositoinForLocals(localKeys, css, sourceMap) { 57 | const locals = {}; 58 | let consumer = null; 59 | let location = null; 60 | if (sourceMap != null) { 61 | location = vfileLocation(vfile(css)); 62 | consumer = await new SourceMapConsumer(sourceMap); 63 | } 64 | Object.keys(localKeys).map(key => { 65 | if (sourceMap != null) { 66 | const positions = getOriginalPositions(consumer, localKeys[key], css, location); 67 | locals[key] = { 68 | name: localKeys[key], 69 | positions, 70 | } 71 | } else { 72 | locals[key] = { 73 | name: localKeys[key], 74 | } 75 | } 76 | }) 77 | return locals; 78 | } 79 | 80 | export default async function processLess(source, rootPath, filePath, camelCase, modulePath) { 81 | try { 82 | const lessResult = await less.render(source, { 83 | sourceMap: { 84 | outputSourceFiles: true 85 | }, 86 | relativeUrls: true, 87 | plugins: [LessImportPlugin(modulePath)], 88 | rootpath: rootPath, 89 | filename: filePath, 90 | }); 91 | 92 | const sourceMap = lessResult.map; 93 | const css = lessResult.css; 94 | 95 | const localKeys = await getLocals(css, camelCase); 96 | const locals = await addPositoinForLocals(localKeys, css, sourceMap); 97 | return { 98 | locals, 99 | css, 100 | source, 101 | } 102 | 103 | } catch (err) { 104 | console.log(err); 105 | } 106 | } 107 | 108 | // 在css文件中 找.xxxx 定位位置,在less中不行,因为有可能xxx在less中是一个变量,此时xxx在css中是一定会出现的,可以通过sourcemap找到css和less文件的对应 -------------------------------------------------------------------------------- /src/parse/index.ts: -------------------------------------------------------------------------------- 1 | import * as path from "path"; 2 | import { compile as JavascriptCompiler } from "./javascript"; 3 | import { compile as TypescriptCompiler } from "./typescript"; 4 | import { PropertyAccessExpression, StyleImport } from "../typings"; 5 | 6 | export function compile(code: string, filepath: string, styleImports: StyleImport[]): PropertyAccessExpression[] { 7 | switch (path.extname(filepath)) { 8 | case ".js": 9 | case ".jsx": 10 | case ".mjs": 11 | return JavascriptCompiler(code, filepath, styleImports); 12 | case ".ts": 13 | case ".tsx": 14 | return TypescriptCompiler(code, filepath, styleImports); 15 | default: 16 | return []; 17 | } 18 | } -------------------------------------------------------------------------------- /src/parse/javascript.ts: -------------------------------------------------------------------------------- 1 | import { 2 | MemberExpression 3 | // isIdentifier, 4 | // isStringLiteral 5 | } from "@babel/types"; 6 | 7 | import { parse } from "babylon"; 8 | import { StyleImport, PropertyAccessExpression } from "../typings"; 9 | const traverse = require("@babel/traverse").default; 10 | 11 | export function compile(code: string, filepath: string, styleImports: StyleImport[]): PropertyAccessExpression[] { 12 | const result = []; 13 | let ast; 14 | try { 15 | ast = parse(code, { 16 | sourceType: "module", 17 | plugins: [ 18 | "jsx", 19 | "flow", 20 | "classConstructorCall", 21 | "doExpressions", 22 | "objectRestSpread", 23 | "decorators", 24 | "classProperties", 25 | "exportExtensions", 26 | "asyncGenerators", 27 | "functionBind", 28 | "functionSent", 29 | "dynamicImport" 30 | ] 31 | }); 32 | } catch (err) { 33 | return void 0; 34 | } 35 | 36 | const visitor: any = { 37 | MemberExpression(object) { 38 | const left = object.node.object.name; 39 | const property = object.node.property; 40 | const matchStyleImport = styleImports.find(si => si.identifier === left) 41 | if (matchStyleImport != null) { 42 | result.push({ 43 | left, 44 | right: property.name, 45 | pos: property.start, 46 | end: property.end, 47 | styleImport: matchStyleImport 48 | }) 49 | } 50 | } 51 | }; 52 | 53 | traverse(ast, visitor); 54 | 55 | return result; 56 | } -------------------------------------------------------------------------------- /src/parse/typescript.ts: -------------------------------------------------------------------------------- 1 | import * as ts from "typescript"; 2 | import { StyleImport, PropertyAccessExpression } from "../typings"; 3 | 4 | export function compile(code: string, filepath: string, styleImports: Array) : Array{ 5 | const result = []; 6 | let sourceFile; 7 | try { 8 | sourceFile = ts.createSourceFile( 9 | "test.ts", 10 | code, 11 | ts.ScriptTarget.Latest, 12 | true, 13 | ts.ScriptKind.TSX 14 | ); 15 | } catch (err) { 16 | return void 0; 17 | } 18 | 19 | function doParse(SourceFile: ts.SourceFile) { 20 | doParseNode(SourceFile); 21 | 22 | function doParseNode(node: ts.Node) { 23 | switch (node.kind) { 24 | case ts.SyntaxKind.PropertyAccessExpression: 25 | const left = (node as ts.PropertyAccessExpression).expression.getText(); 26 | const name = (node as ts.PropertyAccessExpression).name; 27 | const matchStyleImport = styleImports.find(si => si.identifier === left) 28 | if (matchStyleImport != null) { 29 | result.push({ 30 | left, 31 | right: name.getText(), 32 | pos: name.pos, 33 | end: name.end, 34 | styleImport: matchStyleImport 35 | }) 36 | } 37 | break; 38 | } 39 | 40 | ts.forEachChild(node, doParseNode); 41 | } 42 | } 43 | 44 | doParse(sourceFile); 45 | 46 | return result; 47 | } -------------------------------------------------------------------------------- /src/typings.ts: -------------------------------------------------------------------------------- 1 | export interface Position { 2 | line: number; // 0-based 3 | column: number; // 0-based 4 | fsPath: string; // fsPath 5 | } 6 | 7 | export interface Local { 8 | name: string; // original name 9 | positions: Array; 10 | } 11 | 12 | export interface StyleObject { 13 | locals: Map; 14 | css: string; 15 | source: string; 16 | } 17 | 18 | export interface StyleImport { 19 | identifier: string; 20 | jsFsPath: string; 21 | styleFsPath: string; 22 | } 23 | 24 | export interface PropertyAccessExpression { 25 | left: string; 26 | right: string; 27 | pos: number; 28 | end: number; 29 | styleImport: StyleImport; 30 | } 31 | -------------------------------------------------------------------------------- /src/util/findImportObject.ts: -------------------------------------------------------------------------------- 1 | import parseImport, { ImportDeclaration } from 'parse-import-es6'; 2 | import * as path from 'path'; 3 | import { StyleImport } from '../typings'; 4 | 5 | const isStyle = /\.(less|css)$/; 6 | 7 | function getIdentifier(imp: ImportDeclaration) { 8 | // namespace import is priority before default import 9 | if (imp.nameSpaceImport) { 10 | return imp.nameSpaceImport.split('as')[1].trim(); 11 | } 12 | return imp.importedDefaultBinding; 13 | } 14 | 15 | function getFilterImports(source: string) { 16 | const imports = parseImport(source); 17 | // should have default import or namespace import, like: 18 | // import a from 'xxx.less' 19 | // import * as a from 'xxx.less' 20 | const filteredImports = imports.filter( 21 | imp => 22 | imp.error === 0 23 | && isStyle.test(imp.moduleSpecifier) 24 | && (imp.importedDefaultBinding != null || imp.nameSpaceImport != null)); 25 | return filteredImports; 26 | } 27 | 28 | export function findMatchModuleSpecifier(source: string, identifier: string) { 29 | const filteredImports = getFilterImports(source); 30 | let result = null; 31 | filteredImports.some(imp => { 32 | const tempi = getIdentifier(imp); 33 | if (identifier === tempi) { 34 | result = imp.moduleSpecifier; 35 | return true; 36 | } 37 | }); 38 | return result; 39 | } 40 | 41 | export function findAllStyleImports(source: string, fsPath: string) : Array{ 42 | const filteredImports = getFilterImports(source); 43 | return filteredImports.map(imp => { 44 | const identifier = getIdentifier(imp); 45 | return { 46 | identifier, 47 | jsFsPath: fsPath, 48 | styleFsPath: path.join(path.dirname(fsPath), imp.moduleSpecifier), 49 | } 50 | }) 51 | } -------------------------------------------------------------------------------- /src/util/getLocals.ts: -------------------------------------------------------------------------------- 1 | const cssLoader = require('css-loader/lib/localsLoader'); 2 | 3 | const camelCase = require("lodash.camelcase"); 4 | 5 | function dashesCamelCase(str) { 6 | return str.replace(/-+(\w)/g, function(match, firstLetter) { 7 | return firstLetter.toUpperCase(); 8 | }); 9 | } 10 | 11 | function runLoader(loader, input, map, addOptions, callback) { 12 | var opt = { 13 | options: { 14 | context: "" 15 | }, 16 | callback: callback, 17 | async: function () { 18 | return callback; 19 | }, 20 | loaders: [{ request: "/path/css-loader" }], 21 | loaderIndex: 0, 22 | context: "", 23 | resource: "test.css", 24 | resourcePath: "test.css", 25 | request: "css-loader!test.css", 26 | emitError: function (message) { 27 | throw new Error(message); 28 | } 29 | }; 30 | Object.keys(addOptions).forEach(function (key) { 31 | opt[key] = addOptions[key]; 32 | }); 33 | loader.call(opt, input, map); 34 | } 35 | 36 | function removeImportfromSouce(source: string) { 37 | const first = source.indexOf('\n'); 38 | const second = source.indexOf('\n', first + 1); 39 | const third = source.indexOf('\n', second + 1); 40 | return source.substring(third + 1, source.length); 41 | } 42 | 43 | function getQuery(camelCaseKey) { 44 | if (camelCaseKey === 'dashesOnly') { 45 | return '?module&camelCase=dashes&localIdentName=_[local]_'; 46 | } 47 | return '?module&camelCase&localIdentName=_[local]_'; 48 | } 49 | 50 | function compileLocals(locals, camelCaseKey) { 51 | const result = {}; 52 | Object.keys(locals).map(key => { 53 | let targetKey; 54 | if (camelCaseKey === 'dashesOnly') { 55 | targetKey = dashesCamelCase(key); 56 | } else { 57 | targetKey = camelCase(key) 58 | } 59 | // if there is a classname b-c in css file, bC must be after b-c 60 | // see file: https://github.com/webpack-contrib/css-loader/blob/master/lib/compile-exports.js 61 | if (key !== targetKey) { 62 | result[targetKey] = key; 63 | } else { 64 | if (!result[targetKey]) { 65 | result[targetKey] = key; 66 | } 67 | } 68 | }); 69 | return result; 70 | } 71 | 72 | export default async function getLocals(source, camelCaseKey) { 73 | return new Promise((resolve, reject) => { 74 | runLoader(cssLoader, source, undefined, { 75 | query: getQuery(camelCase), 76 | }, function (err, output) { 77 | try { 78 | if(err) { 79 | console.log(err); 80 | resolve({}); 81 | } 82 | const moduleFaker = { 83 | exports: {}, 84 | } 85 | const context = new Function('module', output); 86 | context(moduleFaker); 87 | const locals = compileLocals(moduleFaker.exports, camelCaseKey); 88 | resolve(locals); 89 | } catch (error) { 90 | reject(error); 91 | } 92 | }); 93 | }) 94 | } -------------------------------------------------------------------------------- /src/util/getWordBeforeDot.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | /** 4 | * get word before dot 5 | * @param document 6 | * @param dotPosition the position after dot, just like the position of d. (abc.d) 7 | * @returns if before d is not dot or there is no word then return null, otherwise return the word 8 | */ 9 | export default function getWordBeforeDot(document: vscode.TextDocument , dotPosition) { 10 | let word = null; 11 | 12 | const start = new vscode.Position(dotPosition.line, dotPosition.character - 1); 13 | const end = new vscode.Position(dotPosition.line, dotPosition.character); 14 | const charBeforeRange = document.getText(new vscode.Range(start, end)); 15 | 16 | // if is dot, we get the preview word and at least current dotPosition.character >= 2. 17 | if (charBeforeRange === '.' && dotPosition.character >= 2) { 18 | const posiontBeforeDot = new vscode.Position(dotPosition.line, dotPosition.character - 2); 19 | const range = document.getWordRangeAtPosition(posiontBeforeDot); 20 | if (range) { 21 | word = document.getText(new vscode.Range(range.start, range.end)); 22 | if (range.start.character > 0) { 23 | // support vue $style 24 | const $start = new vscode.Position(range.start.line, range.start.character - 1); 25 | const $end = new vscode.Position(range.start.line, range.start.character); 26 | const maybe$ = document.getText(new vscode.Range($start, $end)); 27 | if (maybe$ === '$') { 28 | return '$' + word; 29 | } 30 | } 31 | return word; 32 | }; 33 | } 34 | return word; 35 | } 36 | -------------------------------------------------------------------------------- /src/util/help.ts: -------------------------------------------------------------------------------- 1 | import * as vscode from 'vscode'; 2 | 3 | export function getStringAttr(name: string, uri: vscode.Uri) { 4 | return vscode.workspace.getConfiguration('perfect-css-modules', uri).get(name); 5 | } 6 | -------------------------------------------------------------------------------- /src/util/vueLanguageRegions.ts: -------------------------------------------------------------------------------- 1 | import { TextDocument as VLTTextDocument, Range as VLTRange } from 'vscode-languageserver-types'; 2 | import { getDocumentRegions, LanguageRange } from 'vue-language-server/dist/modes/embeddedSupport'; 3 | 4 | export default function getVueLanguageRegions(text: string) { 5 | const VLTdoc = VLTTextDocument.create('test://test/test.vue', 'vue', 0, text); 6 | const startPos = VLTdoc.positionAt(0); 7 | const endPos = VLTdoc.positionAt(VLTdoc.getText().length); 8 | const regionsClosure = getDocumentRegions(VLTdoc); 9 | const allRegions = regionsClosure.getLanguageRanges(VLTRange.create(startPos, endPos)); 10 | 11 | const getAllStyleRegions = () => { 12 | const regions: LanguageRange[] = []; 13 | for (const c of allRegions) { 14 | if (/^(css|sass|scss|less|postcss|stylus)$/.test(c.languageId)) { 15 | regions.push(c); 16 | } 17 | } 18 | return regions; 19 | }; 20 | 21 | return { 22 | ...regionsClosure, 23 | getAllStyleRegions 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 | import * as testRunner from '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/workspace/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "perfect-css-modules.rootDir": "/src", 3 | "perfect-css-modules.enableDiagnostic": true, 4 | } 5 | -------------------------------------------------------------------------------- /test/workspace/node_modules/test/index.less: -------------------------------------------------------------------------------- 1 | @my-color: #000000; -------------------------------------------------------------------------------- /test/workspace/root.less: -------------------------------------------------------------------------------- 1 | @import '~test/index'; 2 | 3 | .a { 4 | color: #000000; 5 | } -------------------------------------------------------------------------------- /test/workspace/src/css.css: -------------------------------------------------------------------------------- 1 | .kkk-ss { 2 | color: #ffffff; 3 | } 4 | -------------------------------------------------------------------------------- /test/workspace/src/function.less: -------------------------------------------------------------------------------- 1 | .function(@ccc) { 2 | .@{ccc}-ddd { 3 | color: #ffffff; 4 | } 5 | } -------------------------------------------------------------------------------- /test/workspace/src/index.less: -------------------------------------------------------------------------------- 1 | /* 2 | * comment 3 | */ 4 | @import './function.less'; 5 | 6 | .function(aaa); 7 | 8 | .a { 9 | .b-c { 10 | color: #ffffff; 11 | } 12 | 13 | .b-d { 14 | color: #ffffff; 15 | } 16 | } 17 | 18 | .a { 19 | color: #ffffff; 20 | } 21 | 22 | .ab { 23 | color: #ffffff; 24 | } 25 | 26 | // Variables 27 | @my-selector: banner; 28 | 29 | .@{my-selector} { 30 | font-weight: bold; 31 | line-height: 40px; 32 | margin: 0 auto; 33 | } -------------------------------------------------------------------------------- /test/workspace/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as a from './index.less'; 2 | import * as b from './usevariable.less'; 3 | import * as c from './css.css'; 4 | import * as e from '../root.less'; 5 | import * as f from './usenode.less'; 6 | 7 | 8 | a.aaaDdd 9 | a.a 10 | a.banner 11 | b.a 12 | c.kkkSs 13 | e.a 14 | f.a -------------------------------------------------------------------------------- /test/workspace/src/js.js: -------------------------------------------------------------------------------- 1 | import * as a from './index.less'; 2 | import * as b from './usevariable.less'; 3 | import * as c from './css.css'; 4 | import * as d from './test.modules.less'; 5 | import * as e from '../root.less'; 6 | 7 | 8 | a.aaaDdd 9 | a.a 10 | a.banner 11 | b.a 12 | c.kkkSs 13 | d.lllSs 14 | e.a 15 | -------------------------------------------------------------------------------- /test/workspace/src/usenode.less: -------------------------------------------------------------------------------- 1 | @import '~test/index'; 2 | 3 | .a { 4 | color: @my-color; 5 | } -------------------------------------------------------------------------------- /test/workspace/src/usevariable.less: -------------------------------------------------------------------------------- 1 | @import './variable'; 2 | 3 | .a { 4 | color: @color; 5 | } -------------------------------------------------------------------------------- /test/workspace/src/variable.less: -------------------------------------------------------------------------------- 1 | @color: #ffffff; -------------------------------------------------------------------------------- /test/workspace/src/vue/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 20 | 21 | 32 | 33 | 39 | -------------------------------------------------------------------------------- /test/workspace/src/vue/out.modules.less: -------------------------------------------------------------------------------- 1 | .ab { 2 | color: black; 3 | } 4 | -------------------------------------------------------------------------------- /test/workspace/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 | } -------------------------------------------------------------------------------- /test/workspace/typings/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.less' { 2 | interface IClassNames { 3 | [className: string]: string 4 | } 5 | const classNames: IClassNames; 6 | export = classNames; 7 | } 8 | 9 | declare module '*.css' { 10 | interface IClassNames { 11 | [className: string]: string 12 | } 13 | const classNames: IClassNames; 14 | export = classNames; 15 | } -------------------------------------------------------------------------------- /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 | } --------------------------------------------------------------------------------