Skip to content

Commit 54301d4

Browse files
added css class definition searching
1 parent 6da2293 commit 54301d4

File tree

3 files changed

+108
-3
lines changed

3 files changed

+108
-3
lines changed

vscode-ext/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ A tool for analyzing CSS modules in Next.js projects. The linter detects **unuse
66
- Project-wide analysis with warnings displayed in the editor
77
- High performance thanks to Rust-based implementation
88
- Support for `import aliases` from `tsconfig.json`
9-
- Ability to ignore specific warnings
9+
- Ability to ignore specific warnings
10+
- Find definitions for your CSS classes
1011

1112
## 🔹 Usage
1213
Linting runs automatically **on file save**, and warnings are displayed in the workspace.

vscode-ext/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "next-css-lint",
33
"displayName": "Next.Js CSS linter",
44
"description": "Displaying unused CSS classes in Next.Js projects",
5-
"version": "1.7.2",
5+
"version": "1.7.3",
66
"license": "MIT",
77
"repository": "https://github.com/Andcool-Systems/css-linter",
88
"author": {

vscode-ext/src/extension.ts

Lines changed: 105 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import * as vscode from 'vscode';
22
import { exec } from 'child_process';
3-
import { join } from 'path';
3+
import { join, resolve } from 'path';
44
import { binaries, install } from './installer';
55
import os from 'os';
6+
import { existsSync, readFileSync } from 'fs';
67

78
export function activate(context: vscode.ExtensionContext) {
89
const diagnosticCollection = vscode.languages.createDiagnosticCollection('css-linter-diags');
@@ -89,6 +90,13 @@ export function activate(context: vscode.ExtensionContext) {
8990
run_diag();
9091
})
9192
.catch(e => console.error(`[CSS-linter][ERROR]: ${e}`));
93+
94+
const provider = vscode.languages.registerDefinitionProvider(
95+
{ scheme: 'file', language: 'typescriptreact' },
96+
new CssModuleDefinitionProvider()
97+
);
98+
99+
context.subscriptions.push(provider);
92100
}
93101

94102
class CssFixProvider implements vscode.CodeActionProvider {
@@ -123,5 +131,101 @@ class CssFixProvider implements vscode.CodeActionProvider {
123131
}
124132
}
125133

134+
export class CssModuleDefinitionProvider implements vscode.DefinitionProvider {
135+
async provideDefinition(
136+
document: vscode.TextDocument,
137+
position: vscode.Position,
138+
token: vscode.CancellationToken
139+
): Promise<vscode.Definition | null> {
140+
const wordRange = document.getWordRangeAtPosition(position);
141+
if (!wordRange) return null;
142+
143+
const word = document.getText(wordRange);
144+
const text = document.getText();
145+
const lineText = document.lineAt(position.line).text;
146+
147+
const regex = /(\w+)\.(\w+)/g;
148+
let match;
149+
const matches: { objectName: string; propertyName: string }[] = [];
150+
151+
while ((match = regex.exec(lineText)) !== null) {
152+
matches.push({ objectName: match[1], propertyName: match[2] });
153+
}
154+
155+
const matchData = matches.find(m => m.propertyName === word);
156+
if (!matchData) return null;
157+
158+
const { objectName, propertyName } = matchData;
159+
if (propertyName !== word) return null;
160+
161+
const importRegex = new RegExp(`import\\s+${objectName}\\s+from\\s+['"](.*?)['"]`);
162+
const importMatch = text.match(importRegex);
163+
if (!importMatch) return null;
164+
165+
const importPath = importMatch[1];
166+
const resolvedPath = this.resolveImportPath(document, importPath);
167+
if (!resolvedPath) return null;
168+
169+
const allowedExtensions = ['.module.css', '.module.scss'];
170+
if (!allowedExtensions.some(ext => resolvedPath.endsWith(ext))) return null;
171+
172+
const fileContent = readFileSync(resolvedPath, 'utf-8');
173+
const lines = fileContent.split('\n');
174+
175+
for (let i = 0; i < lines.length; i++) {
176+
if (lines[i].includes(`.${propertyName}`)) {
177+
const definitionUri = vscode.Uri.file(resolvedPath);
178+
const definitionPosition = new vscode.Position(i, 0);
179+
return new vscode.Location(definitionUri, definitionPosition);
180+
}
181+
}
182+
183+
return null;
184+
}
185+
186+
private resolveImportPath(document: vscode.TextDocument, importPath: string): string | null {
187+
const workspacePath = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
188+
if (!workspacePath) return null;
189+
190+
const tsConfigPath = join(workspacePath, 'tsconfig.json');
191+
let aliasMap: Record<string, string> = {};
192+
if (existsSync(tsConfigPath)) {
193+
const tsConfig = JSON.parse(readFileSync(tsConfigPath, 'utf-8'));
194+
if (tsConfig.compilerOptions?.paths) {
195+
aliasMap = this.parseTsConfigPaths(tsConfig.compilerOptions.paths, workspacePath);
196+
}
197+
}
198+
199+
for (const alias in aliasMap) {
200+
if (importPath.startsWith(alias)) {
201+
return importPath.replace(alias, aliasMap[alias]);
202+
}
203+
}
204+
205+
if (importPath.startsWith('.')) {
206+
const currentDir = join(document.uri.fsPath, '..');
207+
return resolve(currentDir, importPath);
208+
}
209+
210+
return join(workspacePath, importPath);
211+
}
212+
213+
private parseTsConfigPaths(
214+
paths: Record<string, string[]>,
215+
workspacePath: string
216+
): Record<string, string> {
217+
const aliasMap: Record<string, string> = {};
218+
for (const alias in paths) {
219+
const targetPaths = paths[alias];
220+
if (targetPaths.length > 0) {
221+
const cleanedAlias = alias.replace(/\*$/, ''); // "@/*" → "@/"
222+
const cleanedPath = targetPaths[0].replace(/\*$/, ''); // "./src/*" → "./src/"
223+
aliasMap[cleanedAlias] = join(workspacePath, cleanedPath);
224+
}
225+
}
226+
return aliasMap;
227+
}
228+
}
229+
126230
export function deactivate() {}
127231

0 commit comments

Comments
 (0)