├── .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 | [](https://marketplace.visualstudio.com/items?itemName=wangtao0101.vscode-perfect-css-modules)
3 | [](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 | 
17 |
18 | ## go to definition
19 | 
20 |
21 | ## diagnostic
22 | 
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 | 
42 |
43 | support autocomplete for $style in script and support es module style
44 | 
45 |
46 | ## vue sfc go to definition
47 | in vue sfc file
48 | 
49 |
50 | goto style file
51 | 
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 |
2 |
3 |
4 | asdf
5 |
6 | {{ $style.ab }}
7 |
asdf
8 |
9 |
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 | }
--------------------------------------------------------------------------------