From f25c5f32470e11e13cb46813fb719f460f1b068c Mon Sep 17 00:00:00 2001 From: Vijaya Krishna <8697234+Viijay-Kr@users.noreply.github.com> Date: Tue, 5 Mar 2024 19:30:10 +0100 Subject: [PATCH] feat(CSS): rename selector across references (#137) --- .changeset/afraid-lions-watch.md | 5 + .../react-app/src/test/SyntaxHighlight.tsx | 1 + .../VariousSelectors/VariousSelectors.tsx | 1 + package-lock.json | 4 +- package.json | 7 + src/extension.ts | 10 +- src/parser/utils.ts | 4 + src/parser/v2/css.ts | 2 + src/providers/css/CSSProvider.ts | 489 +++++++++++------- src/providers/css/codelens.ts | 6 +- src/providers/css/colors.ts | 6 +- src/providers/css/completion.ts | 4 +- src/providers/css/definition.ts | 4 +- src/providers/css/references.ts | 4 +- src/providers/css/rename-selector.ts | 63 +++ src/settings/index.ts | 7 + src/test/suite/extension.test.ts | 163 +++--- 17 files changed, 514 insertions(+), 266 deletions(-) create mode 100644 .changeset/afraid-lions-watch.md create mode 100644 src/providers/css/rename-selector.ts diff --git a/.changeset/afraid-lions-watch.md b/.changeset/afraid-lions-watch.md new file mode 100644 index 0000000..b6d3d94 --- /dev/null +++ b/.changeset/afraid-lions-watch.md @@ -0,0 +1,5 @@ +--- +"react-ts-css": patch +--- + +feat(CSS): rename selector across references diff --git a/examples/react-app/src/test/SyntaxHighlight.tsx b/examples/react-app/src/test/SyntaxHighlight.tsx index b3a9506..425e03a 100644 --- a/examples/react-app/src/test/SyntaxHighlight.tsx +++ b/examples/react-app/src/test/SyntaxHighlight.tsx @@ -9,5 +9,6 @@ export default function SyntaxHighlight() {

+

; } diff --git a/examples/react-app/src/test/VariousSelectors/VariousSelectors.tsx b/examples/react-app/src/test/VariousSelectors/VariousSelectors.tsx index 25e2aee..d7fa69e 100644 --- a/examples/react-app/src/test/VariousSelectors/VariousSelectors.tsx +++ b/examples/react-app/src/test/VariousSelectors/VariousSelectors.tsx @@ -7,5 +7,6 @@ export const VariousSelector = () => {
+
; }; diff --git a/package-lock.json b/package-lock.json index fa832f0..e102204 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "react-ts-css", - "version": "2.4.1", + "version": "2.6.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "react-ts-css", - "version": "2.4.1", + "version": "2.6.0", "license": "MIT", "dependencies": { "typescript-cleanup-definitions": "^1.1.0" diff --git a/package.json b/package.json index 379cc17..8993b96 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,12 @@ "title": "Code Lens for selectors", "default": false, "description": "Codelenses to references of selectors" + }, + "reactTsScss.renameSelector": { + "type": "boolean", + "title": "Rename Selector", + "default": true, + "description": "Rename selectors across multiple locations" } } }, @@ -161,6 +167,7 @@ "watch-tests": "tsc -p . -w --outDir out", "pretest": "npm run compile-tests && npm run compile && npm run lint", "lint": "eslint src --ext ts", + "lint:fix": "eslint src --fix --ext ts", "test": "node ./out/test/runTest.js", "publish:vscode": "vsce publish", "publish:openvsx": "ovsx publish", diff --git a/src/extension.ts b/src/extension.ts index 19333bb..fe580a1 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -7,20 +7,21 @@ import { languages, extensions, } from "vscode"; +import Settings, { EXT_NAME, getSettings } from "./settings"; +import Store from "./store/Store"; import { DefnitionProvider } from "./providers/ts/definitions"; import { HoverProvider } from "./providers/ts/hover"; import { SelectorsCompletionProvider, ImportCompletionProvider, } from "./providers/ts/completion"; -import Settings, { EXT_NAME, getSettings } from "./settings"; -import Store from "./store/Store"; import { DiagnosticCodeAction } from "./providers/ts/code-actions"; import { CssDocumentColorProvider } from "./providers/css/colors"; import { CssVariablesCompletion } from "./providers/css/completion"; import { CssDefinitionProvider } from "./providers/css/definition"; import { ReferenceProvider } from "./providers/css/references"; import { ReferenceCodeLensProvider } from "./providers/css/codelens"; +import { RenameSelectorProvider } from "./providers/css/rename-selector"; const documentSelector = [ { scheme: "file", language: "typescriptreact" }, @@ -150,6 +151,10 @@ export async function activate(context: ExtensionContext): Promise { cssModulesDocumentSelector, new ReferenceCodeLensProvider() ); + const _cssRenameSelectorProvider = languages.registerRenameProvider( + cssModulesDocumentSelector, + new RenameSelectorProvider() + ); context.subscriptions.push(_selectorsCompletionProvider); context.subscriptions.push(_importsCompletionProvider); @@ -161,6 +166,7 @@ export async function activate(context: ExtensionContext): Promise { context.subscriptions.push(_cssDefinitionProvider); context.subscriptions.push(_cssReferenceProvider); context.subscriptions.push(_cssCodeLensProvider); + context.subscriptions.push(_cssRenameSelectorProvider); } catch (e) { console.error(e); window.showWarningMessage( diff --git a/src/parser/utils.ts b/src/parser/utils.ts index 8172f43..fdf125b 100644 --- a/src/parser/utils.ts +++ b/src/parser/utils.ts @@ -88,3 +88,7 @@ export const isNormal = (selector: string) => { export const isCombination = (selector: string) => { return selector.indexOf(".") > -1; }; + +export const stripSelectHelpers = (str: string) => { + return str.replace(/(\&\.)|(\&\-)|(&\s.)|&|^\./gm, ""); +}; diff --git a/src/parser/v2/css.ts b/src/parser/v2/css.ts index 6c23de1..8a91189 100644 --- a/src/parser/v2/css.ts +++ b/src/parser/v2/css.ts @@ -95,6 +95,7 @@ export type Selector = { range: Range; content: string; selectionRange: Range; + rule: string; }; export type Variable = { @@ -133,6 +134,7 @@ export const getSelectors = (ast: Stylesheet, document: TextDocument) => { range, content: parentNode.getText(), selectionRange, + rule: selectorNode.getText(), }); }; diff --git a/src/providers/css/CSSProvider.ts b/src/providers/css/CSSProvider.ts index adda525..c280a59 100644 --- a/src/providers/css/CSSProvider.ts +++ b/src/providers/css/CSSProvider.ts @@ -11,6 +11,7 @@ import { LocationLink, Location, Uri, + WorkspaceEdit, } from "vscode"; import { createStyleSheet, @@ -26,24 +27,25 @@ import Store from "../../store/Store"; import { Function, Node, NodeType } from "../../css-node.types"; import { isColorString, + isSuffix, rangeLooseEqual, rangeStrictEqual, + stripSelectHelpers, toColorCode, - toVsCodePosition, toVsCodeRange, } from "../../parser/utils"; import { TextDocument as css_TextDocument } from "vscode-css-languageservice"; import { ProviderKind } from "../types"; -import { - isIdentifier, - isImportDeclaration, - isImportDefaultSpecifier, - isStringLiteral, -} from "@babel/types"; +import { isIdentifier, isStringLiteral } from "@babel/types"; import path = require("path"); import { ReferenceCodeLens } from "./codelens"; import { readFile } from "fs/promises"; +type CSSProviderOptions = { + providerKind: ProviderKind; + position: Position; + document?: TextDocument; +}; export class CSSProvider { public providerKind: ProviderKind = ProviderKind.Invalid; /** Current Active Position in the Document */ @@ -52,11 +54,7 @@ export class CSSProvider { public document: TextDocument; public static PEEK_REFERENCES_COMMAND = "peek_lens_references"; - public constructor(options: { - providerKind: ProviderKind; - position: Position; - document?: TextDocument; - }) { + public constructor(options: CSSProviderOptions) { this.providerKind = options.providerKind; this.position = options.position; if (options.document) { @@ -66,49 +64,6 @@ export class CSSProvider { } } - public async getCssVariablesForCompletion() { - const module = normalizePath(this.document.uri.fsPath); - const variables: CssParserResult["variables"] = []; - const thisDocPath = this.document.uri.fsPath; - if (module.endsWith(".css")) { - const cssModules = Array.from(Store.cssModules.keys()).filter((c) => - c.endsWith(".css") - ); - await Promise.allSettled( - cssModules.map(async (m) => { - const css_parser_result = await parseCss(m); - if (css_parser_result) { - variables.push(...css_parser_result.variables); - } - }) - ); - } - const completionList = new CompletionList(); - for (const { - name, - value, - location: { uri }, - } of variables) { - if (name && value) { - if (uri.fsPath === thisDocPath) { - continue; - } - const item = new CompletionItem(name, CompletionItemKind.Variable); - const candidate = this.getNodeAtOffset(); - item.detail = value; - item.insertText = candidate?.getText().includes("var") - ? `${name}` - : `var(${name})`; - if (isColorString(value)) { - item.kind = CompletionItemKind.Color; - item.detail = toColorCode(value); - } - completionList.items.push(item); - } - } - return completionList; - } - public getNodeAtOffset(): Node | undefined { const styleSheet = createStyleSheet(this.document); const offset = this.document.offsetAt(this.position); @@ -139,6 +94,200 @@ export class CSSProvider { return; } + public async getReferenceCandidates() { + const candidates: string[] = []; + const filePath = normalizePath(this.document.uri.fsPath); + try { + await Promise.allSettled( + Array.from(Store.tsModules.entries()).map(async ([k, v]) => { + if ([".tsx", ".jsx"].includes(path.extname(v))) { + const document = await readFile(v); + if (document) { + const text = document.toString(); + const fileImportPattern = new RegExp(path.basename(filePath)); + const fileImportMatches = text.match(fileImportPattern); + if (fileImportMatches) { + candidates.push(v); + } + } + } + }) + ); + } catch (e) { + console.error(e); + } + + return candidates; + } + + public async getSelectorRange(): Promise { + const selectorAtRange = await this.getSelectorAtPosition(); + return selectorAtRange ? toVsCodeRange(selectorAtRange.range) : undefined; + } + + public async getSelectorAtPosition() { + const filePath = normalizePath(this.document.uri.fsPath); + const source_css_file = Store.cssModules.get(filePath); + const selectors = (await parseCss(source_css_file ?? ""))?.selectors; + const range = this.document.getWordRangeAtPosition(this.position); + let selectorAtRange: Selector | undefined; + + if (selectors) { + for (const [, value] of selectors.entries()) { + if (range && value.range) { + if (rangeLooseEqual(range, value.range)) { + selectorAtRange = value; + } + } + } + } + + return selectorAtRange; + } + + public async getReferences() { + const referenceCandidates = await this.getReferenceCandidates(); + const references = await Promise.allSettled( + referenceCandidates.map(async (c) => ({ + uri: c, + parsed_result: await Store.parser?.getParsedResultByFile(c), + })) + ).catch((e) => { + throw e; + }); + return references; + } +} + +export class CSSCodeLensProvider extends CSSProvider { + constructor(options: CSSProviderOptions) { + super(options); + } + + public async resolveCodeLens(range: Range): Promise { + let candidates: Location[] = []; + + let selectorAtRange = await this.getSelectorAtPosition(); + const references = await this.getReferences(); + + for (const ref of references) { + if (ref.status === "fulfilled") { + const parsedResult = ref.value.parsed_result?.parsedResult; + if (parsedResult) { + for (const accessor of parsedResult.style_accessors) { + let _selector; + if (isStringLiteral(accessor.property)) { + _selector = accessor.property.value; + } else if (isIdentifier(accessor.property)) { + _selector = accessor.property.name; + } + if (selectorAtRange?.selector === _selector) { + const preferedRange = (() => { + return new Range( + new Position( + accessor.property.loc!.start.line - 1, + accessor.property.loc!.start.column + ), + new Position( + accessor.property.loc!.end.line - 1, + accessor.property.loc!.end.column + ) + ); + })(); + candidates.push( + new Location(Uri.file(ref.value.uri), preferedRange) + ); + } + } + } + } + } + return candidates; + } + + public async provideCodeLenses(): Promise { + const filePath = normalizePath(this.document.uri.fsPath); + const source_css_file = Store.cssModules.get(filePath); + const selectors = (await parseCss(source_css_file ?? ""))?.selectors; + const codeLens: ReferenceCodeLens[] = []; + if (selectors) { + for (const [, _selector] of selectors?.entries()) { + const range = toVsCodeRange(_selector.range); + codeLens.push( + new ReferenceCodeLens(this.document, this.document.fileName, range) + ); + } + } + return codeLens; + } +} + +export class CSSReferenceProvider extends CSSProvider { + constructor(options: CSSProviderOptions) { + super(options); + } + + public async provideReferences( + onlySelectorRange?: boolean + ): Promise { + let candidates: Location[] = []; + let selectorAtRange = await this.getSelectorAtPosition(); + const references = await this.getReferences(); + + for (const ref of references) { + if (ref.status === "fulfilled") { + const parsedResult = ref.value.parsed_result?.parsedResult; + if (parsedResult) { + for (const accessor of parsedResult.style_accessors) { + let _selector; + if (isStringLiteral(accessor.property)) { + _selector = accessor.property.value; + } else if (isIdentifier(accessor.property)) { + _selector = accessor.property.name; + } + if (selectorAtRange?.selector === _selector) { + const preferedRange = (() => { + if (onlySelectorRange) { + return new Range( + new Position( + accessor.property.loc!.start.line - 1, + accessor.property.loc!.start.column + ), + new Position( + accessor.property.loc!.end.line - 1, + accessor.property.loc!.end.column + ) + ); + } + return new Range( + new Position( + accessor.object.loc!.start.line - 1, + accessor.object.loc!.start.column + ), + new Position( + accessor.property.loc!.end.line - 1, + accessor.property.loc!.end.column + ) + ); + })(); + candidates.push( + new Location(Uri.file(ref.value.uri), preferedRange) + ); + } + } + } + } + } + + return candidates; + } +} + +export class CSSColorInfoProvider extends CSSProvider { + constructor(options: CSSProviderOptions) { + super(options); + } + public async provideColorInformation(): Promise { const colorInformation: ColorInformation[] = []; const colorVariables: Set = new Set(); @@ -205,7 +354,9 @@ export class CSSProvider { // @ts-ignore return ls.getColorPresentations(_document, stylesheet, color, range); } +} +export class CSSDefinitionProvider extends CSSProvider { public async provideDefinitions(): Promise { const nodeAtOffset = this.getNodeAtOffset(); const candidates: LocationLink[] = []; @@ -241,168 +392,128 @@ export class CSSProvider { } return candidates; } +} - public async provideReferences(): Promise { - const range = this.document.getWordRangeAtPosition(this.position); - const referenceCandidates = await this.getReferenceCandidates(); - const candidates: Location[] = []; - const filePath = normalizePath(this.document.uri.fsPath); - const css_parser_result = await parseCss(filePath); - const selectors = css_parser_result?.selectors; - let selectorAtRange: Selector | undefined; - - if (selectors) { - for (const [, value] of selectors.entries()) { - if (range && value.range) { - if (rangeLooseEqual(range, value.range)) { - selectorAtRange = value; +export class CSSVariableCompletionProvider extends CSSProvider { + constructor(options: CSSProviderOptions) { + super(options); + } + public async getCssVariablesForCompletion() { + const module = normalizePath(this.document.uri.fsPath); + const variables: CssParserResult["variables"] = []; + const thisDocPath = this.document.uri.fsPath; + if (module.endsWith(".css")) { + const cssModules = Array.from(Store.cssModules.keys()).filter((c) => + c.endsWith(".css") + ); + await Promise.allSettled( + cssModules.map(async (m) => { + const css_parser_result = await parseCss(m); + if (css_parser_result) { + variables.push(...css_parser_result.variables); } - } - } - } - if (!selectorAtRange) { - return []; + }) + ); } - const references = await Promise.allSettled( - referenceCandidates.map(async (c) => ({ - uri: c, - parsed_result: await Store.parser?.getParsedResultByFile(c), - })) - ); - - for (const ref of references) { - if (ref.status === "fulfilled") { - const parsedResult = ref.value.parsed_result?.parsedResult; - if (parsedResult) { - for (const accessor of parsedResult.style_accessors) { - let _selector; - if (isStringLiteral(accessor.property)) { - _selector = accessor.property.value; - } else if (isIdentifier(accessor.property)) { - _selector = accessor.property.name; - } - if (selectorAtRange.selector === _selector) { - const preferedRange = (() => { - return new Range( - new Position( - accessor.object.loc!.start.line - 1, - accessor.object.loc!.start.column - ), - new Position( - accessor.property.loc!.end.line - 1, - accessor.property.loc!.end.column - ) - ); - })(); - candidates.push( - new Location(Uri.file(ref.value.uri), preferedRange) - ); - } - } + const completionList = new CompletionList(); + for (const { + name, + value, + location: { uri }, + } of variables) { + if (name && value) { + if (uri.fsPath === thisDocPath) { + continue; + } + const item = new CompletionItem(name, CompletionItemKind.Variable); + const candidate = this.getNodeAtOffset(); + item.detail = value; + item.insertText = candidate?.getText().includes("var") + ? `${name}` + : `var(${name})`; + if (isColorString(value)) { + item.kind = CompletionItemKind.Color; + item.detail = toColorCode(value); } + completionList.items.push(item); } } - - return candidates; + return completionList; } +} - public async resolveCodeLens(range: Range): Promise { - let candidates: Location[] = []; - const filePath = normalizePath(this.document.uri.fsPath); - const css_parser_result = await parseCss(filePath); - const selectors = css_parser_result?.selectors; - const referenceCandidates = await this.getReferenceCandidates(); - let selectorAtRange: Selector | undefined; - console.log(range, this.document.getText(range)); - if (selectors) { - for (const [, value] of selectors.entries()) { - if (range && value.range) { - if (rangeLooseEqual(range, value.range)) { - selectorAtRange = value; - } - } - } - } +export class CSSRenameProvider extends CSSProvider { + constructor(options: CSSProviderOptions) { + super(options); + } - const references = await Promise.allSettled( - referenceCandidates.map(async (c) => ({ - uri: c, - parsed_result: await Store.parser?.getParsedResultByFile(c), - })) - ); + public async provideRenameReferences( + newName: string + ): Promise> { + const candidates: Array = []; + let selectorAtPosition = await this.getSelectorAtPosition(); + let range = await this.getSelectorRange(); + const references = await this.getReferences(); for (const ref of references) { if (ref.status === "fulfilled") { const parsedResult = ref.value.parsed_result?.parsedResult; if (parsedResult) { for (const accessor of parsedResult.style_accessors) { let _selector; + let selectorType: "Literal" | "Identifier" | undefined; if (isStringLiteral(accessor.property)) { _selector = accessor.property.value; + selectorType = "Literal"; } else if (isIdentifier(accessor.property)) { _selector = accessor.property.name; + selectorType = "Identifier"; } - if (selectorAtRange?.selector === _selector) { + if (selectorAtPosition?.selector === _selector) { const preferedRange = (() => { - return new Range( - new Position( - accessor.property.loc!.start.line - 1, - accessor.property.loc!.start.column - ), - new Position( - accessor.property.loc!.end.line - 1, - accessor.property.loc!.end.column - ) - ); + if (selectorType === "Literal") { + return new Range( + new Position( + accessor.property.loc!.start.line - 1, + accessor.property.loc!.start.column + 1 + ), + new Position( + accessor.property.loc!.end.line - 1, + accessor.property.loc!.end.column - 1 + ) + ); + } else if (selectorType === "Identifier") { + return new Range( + new Position( + accessor.property.loc!.start.line - 1, + accessor.property.loc!.start.column + ), + new Position( + accessor.property.loc!.end.line - 1, + accessor.property.loc!.end.column + ) + ); + } })(); - candidates.push( - new Location(Uri.file(ref.value.uri), preferedRange) - ); + if (preferedRange) { + let previous = stripSelectHelpers(this.document.getText(range)); + let replacement = stripSelectHelpers(newName); + let loc = new Location(Uri.file(ref.value.uri), preferedRange); + candidates.push({ + ...loc, + text: isSuffix(newName) + ? `${selectorAtPosition?.selector.replace( + previous, + "" + )}${replacement}` + : replacement, + }); + } } } } } } - return candidates; - } - - public async getReferenceCandidates() { - const candidates: string[] = []; - const filePath = normalizePath(this.document.uri.fsPath); - try { - await Promise.allSettled( - Array.from(Store.tsModules.entries()).map(async ([k, v]) => { - if ([".tsx", ".jsx"].includes(path.extname(v))) { - const document = await readFile(v); - if (document) { - const text = document.toString(); - const fileImportPattern = new RegExp(path.basename(filePath)); - const fileImportMatches = text.match(fileImportPattern); - if (fileImportMatches) { - candidates.push(v); - } - } - } - }) - ); - } catch (e) { - console.error(e); - } return candidates; } - public async provideCodeLenses(): Promise { - const filePath = normalizePath(this.document.uri.fsPath); - const source_css_file = Store.cssModules.get(filePath); - const selectors = (await parseCss(source_css_file ?? ""))?.selectors; - const codeLens: ReferenceCodeLens[] = []; - if (selectors) { - for (const [, _selector] of selectors?.entries()) { - const range = toVsCodeRange(_selector.range); - codeLens.push( - new ReferenceCodeLens(this.document, this.document.fileName, range) - ); - } - } - return codeLens; - } } diff --git a/src/providers/css/codelens.ts b/src/providers/css/codelens.ts index 0578115..5779664 100644 --- a/src/providers/css/codelens.ts +++ b/src/providers/css/codelens.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; import Settings from "../../settings"; import { ProviderKind } from "../types"; -import { CSSProvider } from "./CSSProvider"; +import { CSSCodeLensProvider, CSSProvider } from "./CSSProvider"; export class ReferenceCodeLens extends vscode.CodeLens { constructor( @@ -22,7 +22,7 @@ export class ReferenceCodeLensProvider implements vscode.CodeLensProvider { return []; } try { - const provider = new CSSProvider({ + const provider = new CSSCodeLensProvider({ providerKind: ProviderKind.CodeLens, document, position: new vscode.Position(0, 0), @@ -45,7 +45,7 @@ export class ReferenceCodeLensProvider implements vscode.CodeLensProvider { try { const uri = codeLens.document.uri; const position = codeLens.range.start; - const provider = new CSSProvider({ + const provider = new CSSCodeLensProvider({ providerKind: ProviderKind.CodeLens, document: codeLens.document, position: codeLens.range.start, diff --git a/src/providers/css/colors.ts b/src/providers/css/colors.ts index edfd18b..2e181ca 100644 --- a/src/providers/css/colors.ts +++ b/src/providers/css/colors.ts @@ -11,7 +11,7 @@ import { } from "vscode"; import Settings from "../../settings"; import { ProviderKind } from "../types"; -import { CSSProvider } from "./CSSProvider"; +import { CSSColorInfoProvider, CSSProvider } from "./CSSProvider"; export class CssDocumentColorProvider implements _DocumentColorProvider { async provideDocumentColors( @@ -20,7 +20,7 @@ export class CssDocumentColorProvider implements _DocumentColorProvider { if (!Settings.cssSyntaxColor) { return []; } - const provider = new CSSProvider({ + const provider = new CSSColorInfoProvider({ providerKind: ProviderKind.Colors, document, position: new Position(0, 0), // providing a dummy position as it not needed for document @@ -35,7 +35,7 @@ export class CssDocumentColorProvider implements _DocumentColorProvider { if (!Settings.cssSyntaxColor) { return []; } - const provider = new CSSProvider({ + const provider = new CSSColorInfoProvider({ providerKind: ProviderKind.Colors, document: context.document, position: new Position(0, 0), // providing a dummy position as it not needed for document diff --git a/src/providers/css/completion.ts b/src/providers/css/completion.ts index f7e1ae9..6c792b0 100644 --- a/src/providers/css/completion.ts +++ b/src/providers/css/completion.ts @@ -6,7 +6,7 @@ import { } from "vscode"; import Settings from "../../settings"; import { ProviderKind } from "../types"; -import { CSSProvider } from "./CSSProvider"; +import { CSSProvider, CSSVariableCompletionProvider } from "./CSSProvider"; export class CssVariablesCompletion implements CompletionItemProvider { async provideCompletionItems( @@ -17,7 +17,7 @@ export class CssVariablesCompletion implements CompletionItemProvider { if (!Settings.cssAutoComplete) { return; } - const provider = new CSSProvider({ + const provider = new CSSVariableCompletionProvider({ providerKind: ProviderKind.Completion, position, document, diff --git a/src/providers/css/definition.ts b/src/providers/css/definition.ts index 7b190fa..f1cd5c8 100644 --- a/src/providers/css/definition.ts +++ b/src/providers/css/definition.ts @@ -1,7 +1,7 @@ import * as vscode from "vscode"; import Settings from "../../settings"; import { ProviderKind } from "../types"; -import { CSSProvider } from "./CSSProvider"; +import { CSSDefinitionProvider, CSSProvider } from "./CSSProvider"; export class CssDefinitionProvider implements vscode.DefinitionProvider { async provideDefinition( @@ -11,7 +11,7 @@ export class CssDefinitionProvider implements vscode.DefinitionProvider { if (!Settings.cssDefinitions) { return []; } - const provider = new CSSProvider({ + const provider = new CSSDefinitionProvider({ document, position, providerKind: ProviderKind.Definition, diff --git a/src/providers/css/references.ts b/src/providers/css/references.ts index d0aa33b..e6aaf6b 100644 --- a/src/providers/css/references.ts +++ b/src/providers/css/references.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode"; import Settings from "../../settings"; import Store from "../../store/Store"; import { ProviderKind } from "../types"; -import { CSSProvider } from "./CSSProvider"; +import { CSSProvider, CSSReferenceProvider } from "./CSSProvider"; export class ReferenceProvider implements vscode.ReferenceProvider { provideReferences( @@ -13,7 +13,7 @@ export class ReferenceProvider implements vscode.ReferenceProvider { return []; } try { - const provider = new CSSProvider({ + const provider = new CSSReferenceProvider({ document, position, providerKind: ProviderKind.References, diff --git a/src/providers/css/rename-selector.ts b/src/providers/css/rename-selector.ts new file mode 100644 index 0000000..0fb8819 --- /dev/null +++ b/src/providers/css/rename-selector.ts @@ -0,0 +1,63 @@ +import { + CancellationToken, + Position, + ProviderResult, + Range, + RenameProvider, + TextDocument, + TextEdit, + WorkspaceEdit, +} from "vscode"; +import Settings from "../../settings"; +import { CSSProvider, CSSRenameProvider } from "./CSSProvider"; +import { ProviderKind } from "../types"; +import { isSuffix, stripSelectHelpers } from "../../parser/utils"; +export class RenameSelectorProvider implements RenameProvider { + async provideRenameEdits( + document: TextDocument, + position: Position, + newName: string, + token: CancellationToken + ): Promise { + if (!Settings.renameSelector || token.isCancellationRequested) { + return; + } + const provider = new CSSRenameProvider({ + providerKind: ProviderKind.RenameSelector, + document, + position, + }); + const edits = new WorkspaceEdit(); + + let range = await provider.getSelectorRange(); + const locations = await provider.provideRenameReferences(newName); + if (range) { + edits.set(document.uri, [new TextEdit(range, newName)]); + } + for (const loc of locations) { + edits.set(loc.uri, [new TextEdit(loc.range, loc.text)]); + } + return edits; + } + async prepareRename?( + document: TextDocument, + position: Position, + token: CancellationToken + ): Promise { + if (!Settings.renameSelector || token.isCancellationRequested) { + return; + } + try { + const provider = new CSSRenameProvider({ + providerKind: ProviderKind.RenameSelector, + document, + position, + }); + const range = await provider.getSelectorRange(); + return range; + } catch (e) { + console.error(e); + return; + } + } +} diff --git a/src/settings/index.ts b/src/settings/index.ts index e48480f..b4c8a0c 100644 --- a/src/settings/index.ts +++ b/src/settings/index.ts @@ -110,6 +110,13 @@ export class Settings { public set codeLens(v: Array | undefined) { workspace.getConfiguration(EXT_NAME).update("codelens", v); } + public get renameSelector(): Array | undefined { + return getSettings().get("renameSelector"); + } + + public set renameSelector(v: Array | undefined) { + workspace.getConfiguration(EXT_NAME).update("renameSelector", v); + } } export default new Settings(); diff --git a/src/test/suite/extension.test.ts b/src/test/suite/extension.test.ts index 5206bae..f4119ee 100644 --- a/src/test/suite/extension.test.ts +++ b/src/test/suite/extension.test.ts @@ -32,7 +32,12 @@ import { ReferenceCodeLensProvider, } from "../../providers/css/codelens"; import { parseCss } from "../../parser/v2/css"; -import { CSSProvider } from "../../providers/css/CSSProvider"; +import { + CSSCodeLensProvider, + CSSProvider, + CSSReferenceProvider, + CSSRenameProvider, +} from "../../providers/css/CSSProvider"; import { ProviderKind } from "../../providers/types"; const examplesLocation = "../../../examples/"; @@ -46,6 +51,12 @@ function setWorskpaceFolder(app: string) { }; } +const VariousSelectorsModule = path.join( + __dirname, + examplesLocation, + "react-app/src/test/VariousSelectors/VariousSelectors.module.scss" +); + suite("Extension Test Suite", async () => { window.showInformationMessage("Start all tests."); const AppComponentUri = Uri.file( @@ -81,6 +92,12 @@ suite("Extension Test Suite", async () => { "react-app/src/test/styles/TestStyles.module.scss" ); + const VariousSelectorSCssModule = path.join( + __dirname, + examplesLocation, + "react-app/src/test/VariousSelectors/VariousSelectors.module.scss" + ); + const DiagnosticComponent = Uri.file( path.join( __dirname, @@ -368,11 +385,6 @@ suite("Extension Test Suite", async () => { }); suite("Selector Possibilities", () => { - const SelectorSCssModule = path.join( - __dirname, - examplesLocation, - "react-app/src/test/VariousSelectors/VariousSelectors.module.scss" - ); const CSSModule = path.join( __dirname, examplesLocation, @@ -380,11 +392,11 @@ suite("Extension Test Suite", async () => { ); test("should include normal selectors [no relationship or bound to any rules]", async () => { - const document = await workspace.openTextDocument(SelectorSCssModule); + const document = await workspace.openTextDocument(VariousSelectorsModule); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); const source_css_file = StorageInstance.cssModules.get( - normalizePath(SelectorSCssModule) + normalizePath(VariousSelectorsModule) ); const node = await parseCss(source_css_file ?? ""); assert.notEqual(node, undefined); @@ -399,11 +411,11 @@ suite("Extension Test Suite", async () => { ); }); test("should include selectors from mixins and media queries", async () => { - const document = await workspace.openTextDocument(SelectorSCssModule); + const document = await workspace.openTextDocument(VariousSelectorsModule); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); const source_css_file = StorageInstance.cssModules.get( - normalizePath(SelectorSCssModule) + normalizePath(VariousSelectorsModule) ); const node = await parseCss(source_css_file ?? ""); assert.notEqual(node, undefined); @@ -423,11 +435,11 @@ suite("Extension Test Suite", async () => { }); test("should include selectors from placeholders", async () => { - const document = await workspace.openTextDocument(SelectorSCssModule); + const document = await workspace.openTextDocument(VariousSelectorsModule); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); const source_css_file = StorageInstance.cssModules.get( - normalizePath(SelectorSCssModule) + normalizePath(VariousSelectorsModule) ); const node = await parseCss(source_css_file ?? ""); assert.notEqual(node, undefined); @@ -436,11 +448,11 @@ suite("Extension Test Suite", async () => { }); test("should include suffixed selectors at any depth", async () => { - const document = await workspace.openTextDocument(SelectorSCssModule); + const document = await workspace.openTextDocument(VariousSelectorsModule); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); const source_css_file = StorageInstance.cssModules.get( - normalizePath(SelectorSCssModule) + normalizePath(VariousSelectorsModule) ); const node = await parseCss(source_css_file ?? ""); assert.notEqual(node, undefined); @@ -460,11 +472,11 @@ suite("Extension Test Suite", async () => { }); test("should include camelCased suffixed selectors", async () => { - const document = await workspace.openTextDocument(SelectorSCssModule); + const document = await workspace.openTextDocument(VariousSelectorsModule); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); const source_css_file = StorageInstance.cssModules.get( - normalizePath(SelectorSCssModule) + normalizePath(VariousSelectorsModule) ); const node = await parseCss(source_css_file ?? ""); assert.notEqual(node, undefined); @@ -604,7 +616,7 @@ suite("Extension Test Suite", async () => { const document = await workspace.openTextDocument(TestCssModulePath); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); - const provider = new CSSProvider({ + const provider = new CSSReferenceProvider({ document, position: new Position(3, 11), providerKind: ProviderKind.References, @@ -616,7 +628,7 @@ suite("Extension Test Suite", async () => { const document = await workspace.openTextDocument(TestCssModulePath); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); - const provider = new CSSProvider({ + const provider = new CSSReferenceProvider({ document, position: new Position(11, 11), providerKind: ProviderKind.References, @@ -626,81 +638,110 @@ suite("Extension Test Suite", async () => { }); }); - suite.skip("References", () => { - test("provide references for a selector at a given position", async () => { + suite("Code Lens V2", () => { + test("provide code lens for a selectors in a document", async () => { const document = await workspace.openTextDocument(TestCssModulePath); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); - const provider = new ReferenceProvider(); - const result = await provider.provideReferences( + const provider = new CSSCodeLensProvider({ document, - new Position(3, 11) - ); - assert.equal((result ?? []).length, 1); + position: new Position(0, 0), + providerKind: ProviderKind.CodeLens, + }); + const result = await provider.provideCodeLenses(); + assert.equal((result ?? []).length > 0, true); }); - test("provide references for a suffix selector at a given position from multiple modules", async () => { + test("provide code lens for a suffix selector in a document", async () => { const document = await workspace.openTextDocument(TestCssModulePath); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); - const provider = new ReferenceProvider(); - const result = await provider.provideReferences( + let provider = new CSSCodeLensProvider({ document, - new Position(11, 11) + position: new Position(0, 0), + providerKind: ProviderKind.CodeLens, + }); + const lenses = await provider.provideCodeLenses(); + provider = new CSSCodeLensProvider({ + providerKind: ProviderKind.CodeLens, + document, + position: new Position(11, 3), + }); + const result = await provider.resolveCodeLens( + document.getWordRangeAtPosition(new Position(11, 3))! ); - assert.equal(result?.length, 4); + assert.equal(result.length, 4); }); }); - suite("Code Lens V2", () => { - test("provide reference code lens for a selectors in a document", async () => { + suite("Rename Selectors", () => { + test("should rename a selector at a given position across all its usage", async () => { const document = await workspace.openTextDocument(TestCssModulePath); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); - const provider = new CSSProvider({ + const provider = new CSSRenameProvider({ document, - position: new Position(0, 0), - providerKind: ProviderKind.CodeLens, + position: new Position(16, 0), + providerKind: ProviderKind.RenameSelector, }); - const result = await provider.provideCodeLenses(); - assert.equal((result ?? []).length > 0, true); + const locations = await provider.provideRenameReferences( + "testCamelCaseRenamed" + ); + assert.equal(locations.length, 1); + assert.equal(locations[0].text, "testCamelCaseRenamed"); + assert.equal(locations[0].range.start.line, 14); + assert.equal(locations[0].range.start.character, 34); + assert.equal(locations[0].range.end.line, 14); + assert.equal(locations[0].range.end.character, 47); }); - test("provide references for a suffix selector in a document", async () => { + + test("should rename a selector at a given position across multiple locations", async () => { const document = await workspace.openTextDocument(TestCssModulePath); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); - const provider = new CSSProvider({ + const provider = new CSSRenameProvider({ document, - position: new Position(0, 0), - providerKind: ProviderKind.CodeLens, + position: new Position(7, 3), + providerKind: ProviderKind.RenameSelector, }); - const lenses = await provider.provideCodeLenses(); - const result = await provider.resolveCodeLens(lenses[3].range); - assert.equal(result.length, 4); + const locations = await provider.provideRenameReferences( + "test-sibling-renamed" + ); + assert.equal(locations.length, 3); }); - }); - suite.skip("Code Lens", () => { - test("provide reference code lens for a selectors in a document", async () => { + test("should rename a suffix selector at a given position with the right combination of parent and suffix", async () => { const document = await workspace.openTextDocument(TestCssModulePath); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); - const provider = new ReferenceCodeLensProvider(); - const result = await provider.provideCodeLenses(document, { - isCancellationRequested: false, - } as CancellationToken); - assert.equal((result ?? []).length > 0, true); + const provider = new CSSRenameProvider({ + document, + position: new Position(11, 3), + providerKind: ProviderKind.RenameSelector, + }); + const locations = await provider.provideRenameReferences( + "&-test-suffix-renamed" + ); + assert.equal(locations.length, 4); + assert.equal(locations[0].text, "test-container-test-suffix-renamed"); }); - test("provide references for a suffix selector in a document", async () => { - const document = await workspace.openTextDocument(TestCssModulePath); + test("should rename a deeply nested suffix selector at a given position with the right combination of parent and suffix", async () => { + const document = await workspace.openTextDocument( + VariousSelectorSCssModule + ); await window.showTextDocument(document); await StorageInstance.experimental_BootStrap(); - const provider = new ReferenceCodeLensProvider(); - const lenses = await provider.provideCodeLenses(document, { - isCancellationRequested: false, - } as CancellationToken); - const result = await provider.resolveCodeLens( - new ReferenceCodeLens(document, document.fileName, lenses[3].range) + let provider = new CSSRenameProvider({ + document, + position: new Position(6, 10), + providerKind: ProviderKind.RenameSelector, + }); + const locations = await provider.provideRenameReferences( + "&-nested-suffix-renamed" + ); + assert.equal(locations.length, 1); + assert.equal( + locations[0].text, + "normal-selector-suffix-nested-suffix-renamed" ); - assert.equal(result.command?.command, "editor.action.showReferences"); }); }); });