diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..2ade3f3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": false, + "tabWidth": 2, + "useTabs": false, + "trailingComma": "es5", + "printWidth": 80 +} diff --git a/.vscode/settings.json b/.vscode/settings.json index b2993b7..61c57ce 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,15 +1,24 @@ // Place your settings in this file to overwrite default and user settings. { - "files.exclude": { - "out": false // set this to true to hide the "out" folder with the compiled JS files - }, - "search.exclude": { - "out": true // set this to false to include "out" folder in search results - }, - // Turn off tsc task auto detection since we have the necessary tasks as npm scripts - "typescript.tsc.autoDetect": "off", - "cssModulesIntellisense.aliases": { - "~": "./src" - }, - "cSpell.words": ["onig", "ovsx", "pcss", "styl", "vsctm"] + "files.exclude": { + "out": false // set this to true to hide the "out" folder with the compiled JS files + }, + "search.exclude": { + "out": true // set this to false to include "out" folder in search results + }, + // Turn off tsc task auto detection since we have the necessary tasks as npm scripts + "typescript.tsc.autoDetect": "off", + "cssModulesIntellisense.aliases": { + "~": "./src" + }, + "cSpell.words": [ + "garg", + "lokesh", + "onig", + "ovsx", + "pcss", + "styl", + "vscodeignore", + "vsctm" + ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index ac801f4..0a40eef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ All notable changes to this project will be documented in this file. ## [Unreleased] +### Fixed + +- Minor Bugs + ## [0.1.5] – 2026-01-27 ### Added @@ -135,5 +139,4 @@ All notable changes to this project will be documented in this file. [0.1.2]: https://github.com/Lokesh-Garg-22/CSS-Modules-IntelliSense/compare/v0.1.1...v0.1.2 [0.1.3]: https://github.com/Lokesh-Garg-22/CSS-Modules-IntelliSense/compare/v0.1.2...v0.1.3 [0.1.4]: https://github.com/Lokesh-Garg-22/CSS-Modules-IntelliSense/compare/v0.1.3...v0.1.4 - [0.1.5]: https://github.com/Lokesh-Garg-22/CSS-Modules-IntelliSense/compare/v0.1.4...v0.1.5 diff --git a/docs/PROJECT_STRUCTURE.md b/docs/PROJECT_STRUCTURE.md index 4466d9b..0cec434 100644 --- a/docs/PROJECT_STRUCTURE.md +++ b/docs/PROJECT_STRUCTURE.md @@ -5,7 +5,7 @@ CSS/SCSS Modules IntelliSense VS Code extension. ## Directory Structure -``` +```txt css-modules-intellisense/ ├── .github/ # GitHub-specific files │ └── workflows/ # GitHub Actions workflows @@ -33,7 +33,8 @@ css-modules-intellisense/ │ │ ├── classNameCache.ts # Class name caching │ │ ├── cssModuleDependencyCache.ts │ │ ├── loadCaches.ts # Cache initialization -│ │ └── processConfig.ts # Configuration processing +│ │ ├── processConfig.ts # Configuration processing +│ │ └── vsConfig.ts # VS Code configuration helpers │ ├── providers/ # Language feature providers │ │ ├── completionProvider.ts # Auto-completion │ │ ├── definitionProvider.ts # Go-to-definition @@ -55,8 +56,7 @@ css-modules-intellisense/ │ │ ├── getPath.ts │ │ ├── getRegistry.ts │ │ ├── isDocumentModule.ts -│ │ ├── isPositionInComment.ts -│ │ ├── isPositionInString.ts +│ │ ├── isPositionInScope.ts │ │ └── sanitizeCssInput.ts │ ├── config.ts # Extension configuration │ └── extension.ts # Extension entry point @@ -70,6 +70,7 @@ css-modules-intellisense/ │ └── scss.tmLanguage.json ├── .editorconfig # Editor configuration ├── .gitignore # Git ignore rules +├── .prettierrc # Prettier formatting rules ├── .markdownlint.json # Markdown linting rules ├── .vscode-test.mjs # VS Code test configuration ├── .vscodeignore # Files to exclude from extension package @@ -120,6 +121,7 @@ Static resources including test fixtures and extension icons ## Configuration Files - `.editorconfig` - Code style consistency +- `.prettierrc` - Code formatting rules - `tsconfig.json` - TypeScript compiler settings - `eslint.config.mjs` - Linting rules - `package.json` - Extension metadata and dependencies diff --git a/docs/TODO.md b/docs/TODO.md index 9acee03..dae2fbc 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -2,7 +2,17 @@ ## Enhancements - +- Add `cssModulesIntellisense.diagnostics.classNotDefined.enabled` (boolean) and + `cssModulesIntellisense.diagnostics.classNotDefined.severity` + (`"error" | "warning" | "info" | "hint"`) + settings to control the "class not defined" diagnostic in `analyzeDocument`. + +- Add `cssModulesIntellisense.diagnostics.classNotUsed.enabled` (boolean) and + `cssModulesIntellisense.diagnostics.classNotUsed.severity` + (`"error" | "warning" | "info" | "hint"`) + settings to show a "Class 'x' is never used" diagnostic on CSS module files, + by cross-referencing `ClassNameCache` against usage in all dependent JS/TS files + via `CssModuleDependencyCache`. ## Known Issues @@ -10,4 +20,7 @@ ## Future Improvements - +- Add an extension setting to configure the class name cache size (LRU max entries). + Currently hardcoded to 3, which causes frequent cache evictions and re-parsing + in projects with more than 3 CSS module files. Should be user-configurable via + `cssModules.classNameCacheSize` in extension settings. diff --git a/eslint.config.mjs b/eslint.config.mjs index d5c0b53..5f53a00 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -1,28 +1,78 @@ import typescriptEslint from "@typescript-eslint/eslint-plugin"; import tsParser from "@typescript-eslint/parser"; -export default [{ - files: ["**/*.ts"], -}, { +export default [ + { + ignores: ["dist/**", "node_modules/**", "assets/**", ".vscode-test/**"], + }, + { + files: ["src/**/*.ts"], plugins: { - "@typescript-eslint": typescriptEslint, + "@typescript-eslint": typescriptEslint, }, - languageOptions: { - parser: tsParser, - ecmaVersion: 2022, - sourceType: "module", + parser: tsParser, + ecmaVersion: 2022, + sourceType: "module", + parserOptions: { + project: true, + tsconfigRootDir: import.meta.dirname, + }, }, - rules: { - "@typescript-eslint/naming-convention": ["warn", { - selector: "import", - format: ["camelCase", "PascalCase"], - }], + // Naming + "@typescript-eslint/naming-convention": [ + "warn", + { selector: "import", format: ["camelCase", "PascalCase"] }, + ], + + // Correctness + curly: "warn", + eqeqeq: ["warn", "always"], + "@typescript-eslint/only-throw-error": "warn", + "@typescript-eslint/no-floating-promises": "error", + "@typescript-eslint/no-misused-promises": "error", + "@typescript-eslint/await-thenable": "error", - curly: "warn", - eqeqeq: "warn", - "no-throw-literal": "warn", - semi: "warn", + // Type safety + "@typescript-eslint/no-explicit-any": "warn", + "@typescript-eslint/no-unsafe-assignment": "warn", + "@typescript-eslint/no-unsafe-member-access": "warn", + "@typescript-eslint/no-unsafe-call": "warn", + "@typescript-eslint/no-unsafe-return": "warn", + + // Dead code + "@typescript-eslint/no-unused-vars": [ + "warn", + { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }, + ], + "no-unused-vars": "off", // disabled in favour of @typescript-eslint/no-unused-vars + + // Code style + semi: "warn", + "@typescript-eslint/consistent-type-imports": [ + "warn", + { prefer: "type-imports", fixStyle: "inline-type-imports" }, + ], + "@typescript-eslint/prefer-nullish-coalescing": "warn", + "@typescript-eslint/prefer-optional-chain": "warn", + }, + }, + { + // Looser rules for config/test files — no type-aware linting needed + files: ["*.mjs", "src/test/**/*.ts"], + plugins: { + "@typescript-eslint": typescriptEslint, + }, + languageOptions: { + parser: tsParser, + ecmaVersion: 2022, + sourceType: "module", + }, + rules: { + curly: "warn", + eqeqeq: "warn", + semi: "warn", }, -}]; \ No newline at end of file + }, +]; diff --git a/src/extension.ts b/src/extension.ts index 358a866..74bb882 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -21,9 +21,9 @@ export async function activate(context: vscode.ExtensionContext) { CheckDocument.diagnosticCollection = diagnosticCollection; Cache.context = context; - const loaded = Cache.loadCache(); + const loaded = await Cache.loadCache(); if (!loaded) { - CssModuleDependencyCache.populateCacheFromWorkspace(); + await CssModuleDependencyCache.populateCacheFromWorkspace(); } loadCaches(); diff --git a/src/libs/cache.ts b/src/libs/cache.ts index 7147494..99afd82 100644 --- a/src/libs/cache.ts +++ b/src/libs/cache.ts @@ -1,9 +1,9 @@ import * as fs from "fs"; import * as path from "path"; -import * as vscode from "vscode"; +import type * as vscode from "vscode"; import { CSS_MODULES_CACHE_FILENAME, DEBOUNCE_TIMER } from "../config"; import { - CacheJsonObject, + type CacheJsonObject, ClassNameCache, ClassNameRangeMap, ModulePathCache, @@ -79,7 +79,7 @@ export default class Cache { static async saveCache() { clearTimeout(this.saveCacheDebounceId); this.saveCacheDebounceId = setTimeout(() => { - this._saveCache(); + this._saveCache().catch(console.error); }, DEBOUNCE_TIMER.CACHE); } @@ -142,10 +142,10 @@ export default class Cache { const parsed: { [K in keyof CacheJsonObject]?: CacheJsonObject[K] } = JSON.parse(raw); - this.pathMapCache.setArray(parsed.pathMapCache || []); + this.pathMapCache.setArray(parsed.pathMapCache ?? []); this.modulePathCache.setMap( - Object.entries(parsed.modulePathCache || {}).map( + Object.entries(parsed.modulePathCache ?? {}).map( ([key, valueArray]) => [ key, new ModulePathCacheSet(this.pathMapCache, valueArray), @@ -154,7 +154,7 @@ export default class Cache { ); this.classNameCache.setMap( - Object.entries(parsed.classNameCache || {}).map(([key, value]) => [ + Object.entries(parsed.classNameCache ?? {}).map(([key, value]) => [ key, new ClassNameRangeMap(Object.entries(value)), ]) diff --git a/src/libs/checkDocument.ts b/src/libs/checkDocument.ts index 46a53e5..10ccfde 100644 --- a/src/libs/checkDocument.ts +++ b/src/libs/checkDocument.ts @@ -60,7 +60,7 @@ export default class CheckDocument { static push(document: vscode.TextDocument): number { if (this.isQueueEmpty()) { const length = this.documentQueue.push(document); - this.checkNextDocument(); + this.checkNextDocument().catch(console.error); return length; } while (this.documentQueue.length >= MAX_CHECK_DOCUMENT_QUEUE_LENGTH) { @@ -109,7 +109,7 @@ export default class CheckDocument { static setDebounceTimer(): void { clearTimeout(this.debounceTimerId); this.debounceTimerId = setTimeout(() => { - this.checkNextDocument(); + this.checkNextDocument().catch(console.error); }, DEBOUNCE_TIMER.CHECK_DOCUMENT); } @@ -137,7 +137,6 @@ export default class CheckDocument { return; } - const text = document.getText(); const diagnostics: vscode.Diagnostic[] = []; const importMatches = await getAllImportModulePaths(document); diff --git a/src/libs/classNameCache.ts b/src/libs/classNameCache.ts index 5f8cba1..e5db822 100644 --- a/src/libs/classNameCache.ts +++ b/src/libs/classNameCache.ts @@ -9,10 +9,10 @@ import { resolveWorkspaceRelativePath, } from "../utils/getPath"; import { sanitizeCssInput } from "../utils/sanitizeCssInput"; -import isPositionInComment from "../utils/isPositionInComment"; +import { isPositionInComment } from "../utils/isPositionInScope"; import CssModuleDependencyCache from "./cssModuleDependencyCache"; import CheckDocument from "./checkDocument"; -import { ClassNameRange, ClassNameRangeMap } from "../types/cache"; +import { type ClassNameRange, ClassNameRangeMap } from "../types/cache"; /** * A utility class to extract and cache class names from CSS Module files. @@ -34,23 +34,29 @@ export default class ClassNameCache { static async updateClassNameCache(e: vscode.TextDocument) { const importPath = getWorkspaceRelativeUriPath(e.uri); clearTimeout(this.ClassNameCacheDebounceIdMap[importPath]); - this.ClassNameCacheDebounceIdMap[importPath] = setTimeout(async () => { - await ClassNameCache.extractFromUri(e.uri); + this.ClassNameCacheDebounceIdMap[importPath] = setTimeout(() => { + (async () => { + await ClassNameCache.extractFromUri(e.uri); - if (SUPPORTED_MODULES.includes(e.languageId)) { - const dependents = CssModuleDependencyCache.getDependentsForDocument(e); + if (SUPPORTED_MODULES.includes(e.languageId)) { + const dependents = + CssModuleDependencyCache.getDependentsForDocument(e); - for (const workspacePath of dependents) { - const resolvedPath = resolveWorkspaceRelativePath(workspacePath); - if (!resolvedPath) { - return; + for (const workspacePath of dependents) { + const resolvedPath = resolveWorkspaceRelativePath(workspacePath); + if (!resolvedPath) { + continue; + } + const document = + await vscode.workspace.openTextDocument(resolvedPath); + CheckDocument.push(document); } - const document = await vscode.workspace.openTextDocument( - resolvedPath - ); - CheckDocument.push(document); } - } + })() + .catch(console.error) + .finally(() => { + delete ClassNameCache.ClassNameCacheDebounceIdMap[importPath]; + }); }, DEBOUNCE_TIMER.UPDATE_CLASS_NAME); } @@ -182,11 +188,11 @@ export default class ClassNameCache { ): Promise { if (Cache.classNameCache.hasByKey(importPath)) { return Array.from( - Cache.classNameCache.getByKey(importPath)?.keys() || [] + Cache.classNameCache.getByKey(importPath)?.keys() ?? [] ); } else { return Array.from( - (await this.extractAndCacheClassNames(importPath))?.keys() || [] + (await this.extractAndCacheClassNames(importPath))?.keys() ?? [] ); } } @@ -279,7 +285,7 @@ export default class ClassNameCache { } Cache.classNameCache.setByKey(importPath, classNames); - Cache.saveCache(); + Cache.saveCache().catch(console.error); return classNames; } } diff --git a/src/libs/cssModuleDependencyCache.ts b/src/libs/cssModuleDependencyCache.ts index 6bc473a..4b85e4f 100644 --- a/src/libs/cssModuleDependencyCache.ts +++ b/src/libs/cssModuleDependencyCache.ts @@ -21,7 +21,7 @@ export default class CssModuleDependencyCache { await this.updateCacheForDocument({ uri }); } - Cache.saveCache(); + Cache.saveCache().catch(console.error); } /** @@ -66,13 +66,11 @@ export default class CssModuleDependencyCache { const sourceFile = getWorkspaceRelativeUriPath(document.uri); let dependents = Cache.modulePathCache.getByKey(relativeImport); - if (!dependents) { - dependents = Cache.modulePathCache.createKey(relativeImport); - } + dependents ??= Cache.modulePathCache.createKey(relativeImport); dependents.addByKey(sourceFile); } - Cache.saveCache(); + Cache.saveCache().catch(console.error); } /** @@ -84,7 +82,7 @@ export default class CssModuleDependencyCache { return ( Cache.modulePathCache .getByKey(getWorkspaceRelativeUriPath(document.uri)) - ?.toKeyArray() || [] + ?.toKeyArray() ?? [] ); } diff --git a/src/libs/loadCaches.ts b/src/libs/loadCaches.ts index 376e8a8..b2590c5 100644 --- a/src/libs/loadCaches.ts +++ b/src/libs/loadCaches.ts @@ -5,36 +5,52 @@ import { registerTriggerOnEdit, registerTriggerOnSave } from "./processConfig"; const loadCaches = () => { // ClassNameCache - registerTriggerOnSave((e) => ClassNameCache.updateClassNameCache(e)); + registerTriggerOnSave((e) => { + ClassNameCache.updateClassNameCache(e).catch(console.error); + }); - registerTriggerOnEdit((e) => ClassNameCache.updateClassNameCache(e.document)); + registerTriggerOnEdit((e) => { + ClassNameCache.updateClassNameCache(e.document).catch(console.error); + }); - vscode.workspace.onDidDeleteFiles((e) => - e.files.forEach((uri) => ClassNameCache.extractFromUri(uri)) - ); + vscode.workspace.onDidDeleteFiles((e) => { + e.files.forEach((uri) => { + ClassNameCache.extractFromUri(uri).catch(console.error); + }); + }); // CssModuleDependencyCache vscode.workspace.onDidCreateFiles((e) => { for (const uri of e.files) { - CssModuleDependencyCache.updateCacheForDocument({ uri }); + CssModuleDependencyCache.updateCacheForDocument({ uri }).catch( + console.error + ); } }); - registerTriggerOnSave((e) => - CssModuleDependencyCache.updateCacheForDocument({ document: e }) - ); + registerTriggerOnSave((e) => { + CssModuleDependencyCache.updateCacheForDocument({ document: e }).catch( + console.error + ); + }); - vscode.workspace.onDidOpenTextDocument((e) => - CssModuleDependencyCache.updateCacheForDocument({ document: e }) - ); + vscode.workspace.onDidOpenTextDocument((e) => { + CssModuleDependencyCache.updateCacheForDocument({ document: e }).catch( + console.error + ); + }); - registerTriggerOnEdit((e) => - CssModuleDependencyCache.updateCacheForDocument({ document: e.document }) - ); + registerTriggerOnEdit((e) => { + CssModuleDependencyCache.updateCacheForDocument({ + document: e.document, + }).catch(console.error); + }); const files = vscode.workspace.textDocuments; for (const file of files) { - CssModuleDependencyCache.updateCacheForDocument({ document: file }); + CssModuleDependencyCache.updateCacheForDocument({ document: file }).catch( + console.error + ); } }; diff --git a/src/libs/processConfig.ts b/src/libs/processConfig.ts index 550e918..e3853df 100644 --- a/src/libs/processConfig.ts +++ b/src/libs/processConfig.ts @@ -1,5 +1,6 @@ import * as vscode from "vscode"; import { CONFIGURATION_KEY, CONFIGURATIONS } from "../config"; +import { getVsConfig } from "./vsConfig"; function registerConditionalListener( event: vscode.Event, @@ -9,8 +10,7 @@ function registerConditionalListener( let disposable: vscode.Disposable | undefined; const applyConfig = () => { - const config = vscode.workspace.getConfiguration(CONFIGURATION_KEY); - const enabled = config.get(settingKey, true); + const enabled = getVsConfig().get(settingKey, true); if (enabled && !disposable) { disposable = event(func); diff --git a/src/libs/vsConfig.ts b/src/libs/vsConfig.ts new file mode 100644 index 0000000..59a82de --- /dev/null +++ b/src/libs/vsConfig.ts @@ -0,0 +1,11 @@ +import * as vscode from "vscode"; +import { CONFIGURATION_KEY, CONFIGURATIONS } from "../config"; + +export const getVsConfig = () => + vscode.workspace.getConfiguration(CONFIGURATION_KEY); + +export const getAliasMap = (): Record => + getVsConfig().get>(CONFIGURATIONS.ALIASES, {}); + +export const getBlacklistPatterns = (): string[] => + getVsConfig().get(CONFIGURATIONS.BLACKLIST_PATTERNS, []); diff --git a/src/providers/completionProvider.ts b/src/providers/completionProvider.ts index 2db798a..7228d27 100644 --- a/src/providers/completionProvider.ts +++ b/src/providers/completionProvider.ts @@ -1,20 +1,16 @@ import * as vscode from "vscode"; -import { CONFIGURATION_KEY, CONFIGURATIONS, MESSAGES } from "../config"; +import { MESSAGES } from "../config"; import ClassNameCache from "../libs/classNameCache"; -import isPositionInString from "../utils/isPositionInString"; +import { + isPositionInString, + isPositionInComment, +} from "../utils/isPositionInScope"; import getImportModulePath from "../utils/getImportModulePath"; -import isPositionInComment from "../utils/isPositionInComment"; import { getWorkspaceRelativeImportPath } from "../utils/getPath"; export default class CompletionItemProvider implements vscode.CompletionItemProvider { - static config = vscode.workspace.getConfiguration(CONFIGURATION_KEY); - static aliasMap = CompletionItemProvider.config.get>( - CONFIGURATIONS.ALIASES, - {} - ); - provideCompletionItems = async ( document: vscode.TextDocument, position: vscode.Position diff --git a/src/providers/definitionProvider.ts b/src/providers/definitionProvider.ts index 87cb115..8568972 100644 --- a/src/providers/definitionProvider.ts +++ b/src/providers/definitionProvider.ts @@ -6,8 +6,10 @@ import { } from "../utils/getPath"; import isDocumentModule from "../utils/isDocumentModule"; import getDataOfClassName from "../utils/getDataOfClassName"; -import isPositionInString from "../utils/isPositionInString"; -import isPositionInComment from "../utils/isPositionInComment"; +import { + isPositionInString, + isPositionInComment, +} from "../utils/isPositionInScope"; import getImportModulePath from "../utils/getImportModulePath"; import getAllImportModulePaths from "../utils/getAllImportModulePaths"; import CssModuleDependencyCache from "../libs/cssModuleDependencyCache"; @@ -96,7 +98,7 @@ export class ModuleDefinitionProvider implements vscode.DefinitionProvider { } const className = cssDoc.getText(wordRange).slice(1); // remove leading dot - const cssPath = resolveImportPathWithAliases(cssDoc, cssDoc.uri.path); + const cssPath = resolveImportPathWithAliases(cssDoc, cssDoc.uri.fsPath); if (!fs.existsSync(cssPath)) { return; } diff --git a/src/providers/renameProvider.ts b/src/providers/renameProvider.ts index d5c8c4e..89c9038 100644 --- a/src/providers/renameProvider.ts +++ b/src/providers/renameProvider.ts @@ -6,10 +6,12 @@ import { resolveWorkspaceRelativePath, } from "../utils/getPath"; import isDocumentModule from "../utils/isDocumentModule"; -import isPositionInString from "../utils/isPositionInString"; +import { + isPositionInString, + isPositionInComment, +} from "../utils/isPositionInScope"; import getDataOfClassName from "../utils/getDataOfClassName"; import getImportModulePath from "../utils/getImportModulePath"; -import isPositionInComment from "../utils/isPositionInComment"; import getAllImportModulePaths from "../utils/getAllImportModulePaths"; import CssModuleDependencyCache from "../libs/cssModuleDependencyCache"; import ClassNameCache from "../libs/classNameCache"; @@ -41,7 +43,7 @@ const provideRenameEdits = async ({ const resolvedPath = resolveImportPathWithAliases(doc, match[2]); if (resolvedPath !== filePath) { - return; + continue; } const classNamePositions = await getDataOfClassName( diff --git a/src/test/extension.test.ts b/src/test/extension.test.ts index 00824ee..754b023 100644 --- a/src/test/extension.test.ts +++ b/src/test/extension.test.ts @@ -2,15 +2,21 @@ import * as assert from "assert"; import * as vscode from "vscode"; import { extensionName, publisher } from "./config"; -suite("Extension Tests", () => { - test("Extension should activate", async () => { +suite("Extension Tests", function () { + this.timeout(30000); + + suiteSetup(async () => { + const ext = vscode.extensions.getExtension(`${publisher}.${extensionName}`); + assert.ok(ext, `Extension "${publisher}.${extensionName}" not found`); + await ext.activate(); + }); + + test("Extension should activate", () => { const ext = vscode.extensions.getExtension(`${publisher}.${extensionName}`); - await ext?.activate(); assert.ok(ext?.isActive); }); test("Run Command Reset Cache", async function () { - this.timeout(10000); const result = await vscode.commands.executeCommand( "css-scss-modules-intellisense.resetCache" ); diff --git a/src/test/providers/completionProvider.test.ts b/src/test/providers/completionProvider.test.ts index da05c87..90dc81b 100644 --- a/src/test/providers/completionProvider.test.ts +++ b/src/test/providers/completionProvider.test.ts @@ -12,17 +12,23 @@ suite("Completion Provider Tests", function () { "assets/fixtures/fixture-1/Sample.jsx" ); - suiteTeardown(async () => { - await vscode.commands.executeCommand("workbench.action.closeAllEditors"); - }); + let doc: vscode.TextDocument; + let editor: vscode.TextEditor; - test("Should provide completion for styles.container", async () => { + suiteSetup(async () => { await vscode.extensions .getExtension(`${publisher}.${extensionName}`) ?.activate(); - const doc = await vscode.workspace.openTextDocument(samplePath); - const editor = await vscode.window.showTextDocument(doc); + doc = await vscode.workspace.openTextDocument(samplePath); + editor = await vscode.window.showTextDocument(doc); + }); + + suiteTeardown(async () => { + await vscode.commands.executeCommand("workbench.action.closeAllEditors"); + }); + + test("Should provide completion for styles.container", async () => { // Go to position after "styles" const lineNum = 3; const pos = new vscode.Position( @@ -33,28 +39,26 @@ suite("Completion Provider Tests", function () { edit.insert(pos, "."); }); - const completions: vscode.CompletionList = - await vscode.commands.executeCommand( - "vscode.executeCompletionItemProvider", - doc.uri, - pos.translate(0, 1) + try { + const completions: vscode.CompletionList = + await vscode.commands.executeCommand( + "vscode.executeCompletionItemProvider", + doc.uri, + pos.translate(0, 1) + ); + assert.ok(completions); + assert.ok(completions!.items.length > 0); + const labels = completions.items.map((item) => item.label); + assert.ok( + labels.includes("container"), + 'Expected to find "container" in completions' ); - assert.ok(completions); - assert.ok(completions!.items.length > 0); - const labels = completions.items.map((item) => item.label); - assert.ok( - labels.includes("container"), - 'Expected to find "container" in completions' - ); + } finally { + await vscode.commands.executeCommand("undo"); + } }); test("Should provide all classes for styles", async () => { - await vscode.extensions - .getExtension(`${publisher}.${extensionName}`) - ?.activate(); - const doc = await vscode.workspace.openTextDocument(samplePath); - const editor = await vscode.window.showTextDocument(doc); - // Go to position after "styles" const lineNum = 3; const pos = new vscode.Position( @@ -65,21 +69,25 @@ suite("Completion Provider Tests", function () { edit.insert(pos, "."); }); - const completions: vscode.CompletionList = - await vscode.commands.executeCommand( - "vscode.executeCompletionItemProvider", - doc.uri, - pos.translate(0, 1) - ); - assert.ok(completions); - assert.ok(completions!.items.length >= 3); - const labels = completions.items.map((item) => item.label); - const expected = ["container", "box", "list"]; - expected.forEach((item) => { - assert.ok( - labels.includes(item), - `Expected to find "${item}" in completions` - ); - }); + try { + const completions: vscode.CompletionList = + await vscode.commands.executeCommand( + "vscode.executeCompletionItemProvider", + doc.uri, + pos.translate(0, 1) + ); + assert.ok(completions); + assert.ok(completions!.items.length >= 3); + const labels = completions.items.map((item) => item.label); + const expected = ["container", "box", "list"]; + expected.forEach((item) => { + assert.ok( + labels.includes(item), + `Expected to find "${item}" in completions` + ); + }); + } finally { + await vscode.commands.executeCommand("undo"); + } }); }); diff --git a/src/test/providers/definitionProvider.test.ts b/src/test/providers/definitionProvider.test.ts index 02f8f99..7243925 100644 --- a/src/test/providers/definitionProvider.test.ts +++ b/src/test/providers/definitionProvider.test.ts @@ -3,6 +3,7 @@ import * as assert from "assert"; import * as vscode from "vscode"; import getRootPath from "../utils/getRootPath"; import { rangeToString } from "../utils/utils"; +import { extensionName, publisher } from "../config"; suite("Definition Provider Tests", function () { this.timeout(60000); @@ -16,13 +17,25 @@ suite("Definition Provider Tests", function () { "assets/fixtures/fixture-2/Sample.module.scss" ); + let jsxDoc: vscode.TextDocument; + let scssDoc: vscode.TextDocument; + + suiteSetup(async () => { + await vscode.extensions + .getExtension(`${publisher}.${extensionName}`) + ?.activate(); + + jsxDoc = await vscode.workspace.openTextDocument(sampleJsxPath); + scssDoc = await vscode.workspace.openTextDocument(sampleScssPath); + }); + suiteTeardown(async () => { await vscode.commands.executeCommand("workbench.action.closeAllEditors"); }); test("Go-to-Definition jumps to .container in SCSS", async () => { - const doc = await vscode.workspace.openTextDocument(sampleJsxPath); - await vscode.window.showTextDocument(doc); + await vscode.window.showTextDocument(jsxDoc); + const doc = jsxDoc; const pos = new vscode.Position(3, doc.lineAt(3).text.indexOf("container")); const locations: vscode.LocationLink[] = await vscode.commands.executeCommand( @@ -65,8 +78,8 @@ suite("Definition Provider Tests", function () { }); test("Go-to-Definition jumps to .container in Script", async () => { - const doc = await vscode.workspace.openTextDocument(sampleScssPath); - await vscode.window.showTextDocument(doc); + await vscode.window.showTextDocument(scssDoc); + const doc = scssDoc; const pos = new vscode.Position(0, doc.lineAt(0).text.indexOf("container")); const locations: vscode.LocationLink[] = await vscode.commands.executeCommand( @@ -82,7 +95,7 @@ suite("Definition Provider Tests", function () { await Promise.all( locations.map(async (defLoc) => { - if (defLoc.targetUri.path.endsWith(sampleJsxPath)) { + if (defLoc.targetUri.fsPath === sampleJsxPath) { assert.ok( defLoc.targetSelectionRange, `Expected Target Selection Range for .container` @@ -108,7 +121,7 @@ suite("Definition Provider Tests", function () { expectedRange )}, but got ${rangeToString(defLoc.targetSelectionRange)}` ); - } else if (defLoc.targetUri.path.endsWith(sampleScssPath)) { + } else if (defLoc.targetUri.fsPath === sampleScssPath) { assert.ok( defLoc.targetSelectionRange, `Expected Target Selection Range for .container` @@ -134,6 +147,10 @@ suite("Definition Provider Tests", function () { expectedRange )}, but got ${rangeToString(defLoc.targetSelectionRange)}` ); + } else { + assert.fail( + `Unexpected definition location: ${defLoc.targetUri.fsPath}` + ); } }) ); diff --git a/src/test/providers/renameProvider.test.ts b/src/test/providers/renameProvider.test.ts index e6c6901..ad4dbfe 100644 --- a/src/test/providers/renameProvider.test.ts +++ b/src/test/providers/renameProvider.test.ts @@ -16,18 +16,24 @@ suite("Rename Provider Tests", function () { "assets/fixtures/fixture-3/Sample.module.scss" ); - suiteTeardown(async () => { - await vscode.commands.executeCommand("workbench.action.closeAllEditors"); - }); + let jsxDoc: vscode.TextDocument; + let scssDoc: vscode.TextDocument; - test("Rename 'container' to 'wrapper' inside CSS Module", async () => { - // Ensure extension is activated + suiteSetup(async () => { await vscode.extensions .getExtension(`${publisher}.${extensionName}`) ?.activate(); - const doc = await vscode.workspace.openTextDocument(sampleScssPath); - const editor = await vscode.window.showTextDocument(doc); + jsxDoc = await vscode.workspace.openTextDocument(sampleJsxPath); + scssDoc = await vscode.workspace.openTextDocument(sampleScssPath); + }); + + suiteTeardown(async () => { + await vscode.commands.executeCommand("workbench.action.closeAllEditors"); + }); + + test("Rename 'container' to 'wrapper' inside CSS Module", async () => { + const doc = scssDoc; const lineNum = 0; const lineText = doc.lineAt(lineNum).text; @@ -52,8 +58,11 @@ suite("Rename Provider Tests", function () { const changes = workspaceEdit!.entries(); const expectedPaths = [ - path.resolve("assets/fixtures/fixture-3/Sample.jsx"), - path.resolve("assets/fixtures/fixture-3/Sample.module.scss"), + path.resolve(getRootPath(), "assets/fixtures/fixture-3/Sample.jsx"), + path.resolve( + getRootPath(), + "assets/fixtures/fixture-3/Sample.module.scss" + ), ].map((p) => vscode.Uri.file(p).fsPath); // normalize for platform const seenPaths = new Set(); @@ -86,13 +95,7 @@ suite("Rename Provider Tests", function () { }); test("Rename 'container' to 'wrapper' inside a Script", async () => { - // Ensure extension is activated - await vscode.extensions - .getExtension(`${publisher}.${extensionName}`) - ?.activate(); - - const doc = await vscode.workspace.openTextDocument(sampleJsxPath); - const editor = await vscode.window.showTextDocument(doc); + const doc = jsxDoc; const lineNum = 3; const lineText = doc.lineAt(lineNum).text; @@ -117,8 +120,11 @@ suite("Rename Provider Tests", function () { const changes = workspaceEdit!.entries(); const expectedPaths = [ - path.resolve("assets/fixtures/fixture-3/Sample.jsx"), - path.resolve("assets/fixtures/fixture-3/Sample.module.scss"), + path.resolve(getRootPath(), "assets/fixtures/fixture-3/Sample.jsx"), + path.resolve( + getRootPath(), + "assets/fixtures/fixture-3/Sample.module.scss" + ), ].map((p) => vscode.Uri.file(p).fsPath); // normalize for platform const seenPaths = new Set(); diff --git a/src/test/utils/getRootPath.ts b/src/test/utils/getRootPath.ts index 26b7c8e..d7f0eac 100644 --- a/src/test/utils/getRootPath.ts +++ b/src/test/utils/getRootPath.ts @@ -1,13 +1,14 @@ +import * as path from "path"; + const getRootPath = () => { const testDir = "dist"; - const path = __dirname.split("/"); + let dir = __dirname; - while (path[path.length - 1] !== testDir) { - path.pop(); + while (path.basename(dir) !== testDir) { + dir = path.dirname(dir); } - path.pop(); - return path.join("/"); + return path.dirname(dir); }; export default getRootPath; diff --git a/src/test/utils/utils.ts b/src/test/utils/utils.ts index b51ee76..3160fbe 100644 --- a/src/test/utils/utils.ts +++ b/src/test/utils/utils.ts @@ -1,4 +1,4 @@ -import * as vscode from "vscode"; +import type * as vscode from "vscode"; export const rangeToString = (range: vscode.Range) => { return `[(${range.start.line}, ${range.start.character}), (${range.end.line}, ${range.end.character})]`; diff --git a/src/types/cache.ts b/src/types/cache.ts index b1eafe0..76949ec 100644 --- a/src/types/cache.ts +++ b/src/types/cache.ts @@ -1,4 +1,4 @@ -import * as vscode from "vscode"; +import type * as vscode from "vscode"; import { LRUCache } from "lru-cache"; export type CacheJsonObject = { @@ -39,6 +39,10 @@ export class PathMapCache extends Array { }); } + hasKey(key: string): boolean { + return this.reverseMap.has(key); + } + getKeyFormIndex(index: number) { return this[index]; } @@ -95,8 +99,11 @@ class BaseCache { } hasByKey(key: string): boolean { - const keyIndex = this.pathMapCache.getIndexFormKey(key); - return this.has(keyIndex); + if (!this.pathMapCache.hasKey(key)) { + return false; + } + + return this.has(this.pathMapCache.getIndexFormKey(key)); } getByKey(key: string) { @@ -110,7 +117,7 @@ class BaseCache { } setMap(entries: readonly (readonly [string, V])[]) { - this.clear; + this.clear(); entries.forEach((entry) => { this.setByKey(entry[0], entry[1]); }); diff --git a/src/types/classNameData.ts b/src/types/classNameData.ts index a6bd579..266d468 100644 --- a/src/types/classNameData.ts +++ b/src/types/classNameData.ts @@ -1,4 +1,4 @@ -import * as vscode from "vscode"; +import type * as vscode from "vscode"; export type ClassNameData = { startPosition: vscode.Position; diff --git a/src/utils/getAllClassNames.ts b/src/utils/getAllClassNames.ts index a816ef5..f1d431d 100644 --- a/src/utils/getAllClassNames.ts +++ b/src/utils/getAllClassNames.ts @@ -1,7 +1,6 @@ import * as vscode from "vscode"; -import isPositionInString from "./isPositionInString"; -import isPositionInComment from "./isPositionInComment"; -import { ClassNameData } from "../types/classNameData"; +import { isPositionInString, isPositionInComment } from "./isPositionInScope"; +import { type ClassNameData } from "../types/classNameData"; /** * Extracts all valid usages of CSS Module class names accessed via a given variable name diff --git a/src/utils/getAllFiles.ts b/src/utils/getAllFiles.ts index f2d32f5..a381ab8 100644 --- a/src/utils/getAllFiles.ts +++ b/src/utils/getAllFiles.ts @@ -3,18 +3,11 @@ import { getModuleFileRegex, getScriptFileRegex, } from "./getFileExtensionRegex"; -import { CONFIGURATION_KEY, CONFIGURATIONS } from "../config"; - -const config = vscode.workspace.getConfiguration(CONFIGURATION_KEY); -const blacklistPatterns = config.get( - CONFIGURATIONS.BLACKLIST_PATTERNS, - [] -); +import { getBlacklistPatterns } from "../libs/vsConfig"; export const getAllScriptFiles = async () => { const includePattern = `**/*.{${getScriptFileRegex()}}`; - - const excludePattern = `{${blacklistPatterns.join(",")}}`; + const excludePattern = `{${getBlacklistPatterns().join(",")}}`; const files = await vscode.workspace.findFiles( includePattern, @@ -26,8 +19,7 @@ export const getAllScriptFiles = async () => { export const getAllModuleFiles = async () => { const includePattern = `**/*.module.{${getModuleFileRegex(",")}}`; - - const excludePattern = `{${blacklistPatterns.join(",")}}`; + const excludePattern = `{${getBlacklistPatterns().join(",")}}`; const files = await vscode.workspace.findFiles( includePattern, diff --git a/src/utils/getAllImportModulePaths.ts b/src/utils/getAllImportModulePaths.ts index 2a03904..e6380dc 100644 --- a/src/utils/getAllImportModulePaths.ts +++ b/src/utils/getAllImportModulePaths.ts @@ -1,7 +1,6 @@ -import * as vscode from "vscode"; +import type * as vscode from "vscode"; import { getModuleFileRegex } from "./getFileExtensionRegex"; -import isPositionInComment from "./isPositionInComment"; -import isPositionInString from "./isPositionInString"; +import { isPositionInString, isPositionInComment } from "./isPositionInScope"; /** * Extracts all import statements from a VS Code document that import CSS Modules. diff --git a/src/utils/getDataOfClassName.ts b/src/utils/getDataOfClassName.ts index 8432249..61be0f7 100644 --- a/src/utils/getDataOfClassName.ts +++ b/src/utils/getDataOfClassName.ts @@ -1,7 +1,6 @@ import * as vscode from "vscode"; -import isPositionInString from "./isPositionInString"; -import isPositionInComment from "./isPositionInComment"; -import { ClassNameData } from "../types/classNameData"; +import { isPositionInString, isPositionInComment } from "./isPositionInScope"; +import { type ClassNameData } from "../types/classNameData"; /** * Finds all usages of a specific CSS Module class name in a document, where the usage diff --git a/src/utils/getGrammar.ts b/src/utils/getGrammar.ts index 82d7b8b..5a51d1c 100644 --- a/src/utils/getGrammar.ts +++ b/src/utils/getGrammar.ts @@ -1,5 +1,5 @@ -import * as vscode from "vscode"; -import * as vsctm from "vscode-textmate"; +import type * as vscode from "vscode"; +import type * as vsctm from "vscode-textmate"; import getRegistry from "./getRegistry"; const grammarMap = new Map(); diff --git a/src/utils/getGrammarTokens.ts b/src/utils/getGrammarTokens.ts index 304bb3a..ed967eb 100644 --- a/src/utils/getGrammarTokens.ts +++ b/src/utils/getGrammarTokens.ts @@ -1,4 +1,4 @@ -import * as vscode from "vscode"; +import type * as vscode from "vscode"; import * as vsctm from "vscode-textmate"; import getGrammar from "./getGrammar"; diff --git a/src/utils/getImportModulePath.ts b/src/utils/getImportModulePath.ts index fb324f0..65f52cc 100644 --- a/src/utils/getImportModulePath.ts +++ b/src/utils/getImportModulePath.ts @@ -1,8 +1,7 @@ -import * as vscode from "vscode"; +import type * as vscode from "vscode"; import getImportModuleVarName from "./getImportModuleVarName"; import { getModuleFileRegex } from "./getFileExtensionRegex"; -import isPositionInComment from "./isPositionInComment"; -import isPositionInString from "./isPositionInString"; +import { isPositionInString, isPositionInComment } from "./isPositionInScope"; /** * Retrieves the import path of a CSS Module associated with the variable diff --git a/src/utils/getImportModuleVarName.ts b/src/utils/getImportModuleVarName.ts index 90388be..91d1076 100644 --- a/src/utils/getImportModuleVarName.ts +++ b/src/utils/getImportModuleVarName.ts @@ -1,4 +1,4 @@ -import * as vscode from "vscode"; +import type * as vscode from "vscode"; /** * Extracts the variable name used to reference a CSS module import @@ -23,7 +23,7 @@ const getImportModuleVarName = ( // Match patterns like "styles.className", but NOT "temp.styles.className" const match = prefix.match(/(\w+)\.([\w-]*)$/); - if (match && typeof match.index !== "undefined") { + if (typeof match?.index !== "undefined") { if (match.index > 0 && prefix[match.index - 1].match(/[.\w]$/)) { return; } diff --git a/src/utils/getPath.ts b/src/utils/getPath.ts index 8fe5b9a..725bccf 100644 --- a/src/utils/getPath.ts +++ b/src/utils/getPath.ts @@ -1,10 +1,6 @@ import * as path from "path"; import * as vscode from "vscode"; -import { CONFIGURATION_KEY, CONFIGURATIONS } from "../config"; - -// Load alias configuration from workspace settings -const config = vscode.workspace.getConfiguration(CONFIGURATION_KEY); -const aliasMap = config.get>(CONFIGURATIONS.ALIASES, {}); +import { getAliasMap } from "../libs/vsConfig"; /** * Resolves an import path by checking configured aliases and falling back to relative resolution from the current document. @@ -24,6 +20,7 @@ export const resolveImportPathWithAliases = ( const workspaceRoot = workspaceFolders[0].uri.fsPath; let resolvedPath = importPath; + const aliasMap = getAliasMap(); // Replace alias with absolute path if match is found for (const [alias, aliasRelativePath] of Object.entries(aliasMap)) { diff --git a/src/utils/isDocumentModule.ts b/src/utils/isDocumentModule.ts index 9b3f556..c770e5b 100644 --- a/src/utils/isDocumentModule.ts +++ b/src/utils/isDocumentModule.ts @@ -1,4 +1,4 @@ -import * as vscode from "vscode"; +import type * as vscode from "vscode"; import { SUPPORTED_MODULE_EXTENSIONS, SUPPORTED_MODULES } from "../config"; /** diff --git a/src/utils/isPositionInComment.ts b/src/utils/isPositionInComment.ts deleted file mode 100644 index 1d986d4..0000000 --- a/src/utils/isPositionInComment.ts +++ /dev/null @@ -1,25 +0,0 @@ -import * as vscode from "vscode"; -import * as vsctm from "vscode-textmate"; -import getGrammarTokens from "./getGrammarTokens"; - -const isPositionInComment = async ( - document: vscode.TextDocument, - position: vscode.Position -): Promise => { - let tokens = await getGrammarTokens(document, position); - return isCharInComment(tokens, position.character); -}; - -const isCharInComment = (tokens: vsctm.IToken[], char: number): boolean => { - for (const token of tokens) { - if ( - (char >= token.startIndex && char < token.endIndex) || - (char === token.endIndex && token === tokens[tokens.length - 1]) - ) { - return token.scopes.some((scope) => scope.includes("comment")); - } - } - return false; -}; - -export default isPositionInComment; diff --git a/src/utils/isPositionInScope.ts b/src/utils/isPositionInScope.ts new file mode 100644 index 0000000..2c2c0a3 --- /dev/null +++ b/src/utils/isPositionInScope.ts @@ -0,0 +1,45 @@ +import type * as vscode from "vscode"; +import type * as vsctm from "vscode-textmate"; +import getGrammarTokens from "./getGrammarTokens"; + +export const isCharInScope = ( + tokens: vsctm.IToken[], + char: number, + scopePredicate: (scopes: string[]) => boolean +): boolean => { + for (const token of tokens) { + if ( + (char >= token.startIndex && char < token.endIndex) || + (char === token.endIndex && token === tokens[tokens.length - 1]) + ) { + return scopePredicate(token.scopes); + } + } + return false; +}; + +export const isPositionInString = async ( + document: vscode.TextDocument, + position: vscode.Position +): Promise => { + const tokens = await getGrammarTokens(document, position); + return isCharInScope(tokens, position.character, (scopes) => { + if ( + scopes.some((s) => s.includes("string.template")) && + scopes.some((s) => s.includes("meta.template.expression")) + ) { + return false; + } + return scopes.some((s) => s.includes("string")); + }); +}; + +export const isPositionInComment = async ( + document: vscode.TextDocument, + position: vscode.Position +): Promise => { + const tokens = await getGrammarTokens(document, position); + return isCharInScope(tokens, position.character, (scopes) => + scopes.some((s) => s.includes("comment")) + ); +}; diff --git a/src/utils/isPositionInString.ts b/src/utils/isPositionInString.ts deleted file mode 100644 index 1896503..0000000 --- a/src/utils/isPositionInString.ts +++ /dev/null @@ -1,32 +0,0 @@ -import * as vscode from "vscode"; -import * as vsctm from "vscode-textmate"; -import getGrammarTokens from "./getGrammarTokens"; - -const isPositionInString = async ( - document: vscode.TextDocument, - position: vscode.Position -): Promise => { - let tokens = await getGrammarTokens(document, position); - return isCharInString(tokens, position.character); -}; - -const isCharInString = (tokens: vsctm.IToken[], char: number): boolean => { - for (const token of tokens) { - if ( - (char >= token.startIndex && char < token.endIndex) || - (char === token.endIndex && token === tokens[tokens.length - 1]) - ) { - if ( - token.scopes.some((scope) => scope.includes("string.template")) && - token.scopes.some((scope) => scope.includes("meta.template.expression")) - ) { - return false; - } else { - return token.scopes.some((scope) => scope.includes("string")); - } - } - } - return false; -}; - -export default isPositionInString; diff --git a/tsconfig.json b/tsconfig.json index 7f94548..2242670 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "compilerOptions": { "target": "ES2020", "module": "commonjs", + "types": ["node", "mocha"], "lib": ["ES2020"], "outDir": "dist", "rootDir": "src",