diff --git a/packages/language-service/lib/plugins/css.ts b/packages/language-service/lib/plugins/css.ts index 9c6128dc2a..e2423bc6d3 100644 --- a/packages/language-service/lib/plugins/css.ts +++ b/packages/language-service/lib/plugins/css.ts @@ -1,5 +1,12 @@ import type { LanguageServicePlugin, LanguageServicePluginInstance } from '@volar/language-service'; -import { create as baseCreate } from 'volar-service-css'; +import type { TextDocument } from 'vscode-languageserver-textdocument'; +import { VueVirtualCode } from '@vue/language-core'; +import { create as baseCreate, type Provide } from 'volar-service-css'; +import * as css from 'vscode-css-languageservice'; +import { URI } from 'vscode-uri'; + +const cssClassNameReg = /(?=(\.[a-z_][-\w]*)[\s.,+~>:#[{])/gi; +const vBindCssVarReg = /\bv-bind\(\s*(?:'([^']+)'|"([^"]+)"|([a-z_]\w*))\s*\)/gi; export function create(): LanguageServicePlugin { const base = baseCreate({ scssDocumentSelector: ['scss', 'postcss'] }); @@ -7,6 +14,11 @@ export function create(): LanguageServicePlugin { ...base, create(context): LanguageServicePluginInstance { const baseInstance = base.create(context); + const { + 'css/languageService': getCssLs, + 'css/stylesheet': getStylesheet + } = baseInstance.provide as Provide; + return { ...baseInstance, async provideDiagnostics(document, token) { @@ -18,7 +30,60 @@ export function create(): LanguageServicePlugin { } return diagnostics; }, + provideRenameRange(document, position) { + + const decoded = context.decodeEmbeddedDocumentUri(URI.parse(document.uri)); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + + const regexps: RegExp[] = []; + + if (virtualCode?.id.startsWith('style_')) { + const i = Number(virtualCode.id.slice('style_'.length)); + if (sourceScript?.generated?.root instanceof VueVirtualCode) { + const style = sourceScript.generated.root._sfc.styles[i]; + const option = sourceScript.generated.root.vueCompilerOptions.experimentalResolveStyleCssClasses; + if (option === 'always' || (option === 'scoped' && style.scoped) || style.module) { + regexps.push(cssClassNameReg); + } + } + } + regexps.push(vBindCssVarReg); + + return worker(document, (stylesheet, cssLs) => { + const text = document.getText(); + const offset = document.offsetAt(position); + + for (const [start, end] of forEachRegExp()) { + if (offset >= start && offset <= end) { + return; + } + } + return cssLs.prepareRename(document, position, stylesheet); + + function* forEachRegExp() { + for (const reg of regexps) { + for (const match of text.matchAll(reg)) { + const matchText = match.slice(1).find(t => t); + if (matchText) { + const start = match.index + text.slice(match.index).indexOf(matchText) + const end = start + matchText.length; + yield [start, end]; + } + } + } + } + }); + } }; + + async function worker(document: TextDocument, callback: (stylesheet: css.Stylesheet, cssLs: css.LanguageService) => T) { + const cssLs = getCssLs(document); + if (!cssLs) { + return; + } + return callback(getStylesheet(document, cssLs), cssLs); + } }, }; } diff --git a/packages/language-service/package.json b/packages/language-service/package.json index 104ef62219..a8000fa750 100644 --- a/packages/language-service/package.json +++ b/packages/language-service/package.json @@ -34,6 +34,7 @@ "volar-service-pug-beautify": "0.0.62", "volar-service-typescript": "0.0.62", "volar-service-typescript-twoslash-queries": "0.0.62", + "vscode-css-languageservice": "^6.3.1", "vscode-html-languageservice": "^5.2.0", "vscode-languageserver-textdocument": "^1.0.11", "vscode-uri": "^3.0.8" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 072b4f4fea..77e9d50e31 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -961,46 +961,55 @@ packages: resolution: {integrity: sha512-ArdGtPHjLqWkqQuoVQ6a5UC5ebdX8INPuJuJNWRe0RGa/YNhVvxeWmCTFQ7LdmNCSUzVZzxAvUznKaYx645Rig==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.24.2': resolution: {integrity: sha512-B6UHHeNnnih8xH6wRKB0mOcJGvjZTww1FV59HqJoTJ5da9LCG6R4SEBt6uPqzlawv1LoEXSS0d4fBlHNWl6iYw==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.24.2': resolution: {integrity: sha512-kr3gqzczJjSAncwOS6i7fpb4dlqcvLidqrX5hpGBIM1wtt0QEVtf4wFaAwVv8QygFU8iWUMYEoJZWuWxyua4GQ==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.24.2': resolution: {integrity: sha512-TDdHLKCWgPuq9vQcmyLrhg/bgbOvIQ8rtWQK7MRxJ9nvaxKx38NvY7/Lo6cYuEnNHqf6rMqnivOIPIQt6H2AoA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-powerpc64le-gnu@4.24.2': resolution: {integrity: sha512-xv9vS648T3X4AxFFZGWeB5Dou8ilsv4VVqJ0+loOIgDO20zIhYfDLkk5xoQiej2RiSQkld9ijF/fhLeonrz2mw==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.24.2': resolution: {integrity: sha512-tbtXwnofRoTt223WUZYiUnbxhGAOVul/3StZ947U4A5NNjnQJV5irKMm76G0LGItWs6y+SCjUn/Q0WaMLkEskg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.24.2': resolution: {integrity: sha512-gc97UebApwdsSNT3q79glOSPdfwgwj5ELuiyuiMY3pEWMxeVqLGKfpDFoum4ujivzxn6veUPzkGuSYoh5deQ2Q==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.24.2': resolution: {integrity: sha512-jOG/0nXb3z+EM6SioY8RofqqmZ+9NKYvJ6QQaa9Mvd3RQxlH68/jcB/lpyVt4lCiqr04IyaC34NzhUqcXbB5FQ==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.24.2': resolution: {integrity: sha512-XAo7cJec80NWx9LlZFEJQxqKOMz/lX3geWs2iNT5CHIERLFfd90f3RYLLjiCBm1IMaQ4VOX/lTC9lWfzzQm14Q==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.24.2': resolution: {integrity: sha512-A+JAs4+EhsTjnPQvo9XY/DC0ztaws3vfqzrMNMKlwQXuniBKOIIvAAI8M0fBYiTCxQnElYu7mLk7JrhlQ+HeOw==}