diff --git a/packages/language-service/lib/plugins/css.ts b/packages/language-service/lib/plugins/css.ts index de1dcaa276..4376e281d9 100644 --- a/packages/language-service/lib/plugins/css.ts +++ b/packages/language-service/lib/plugins/css.ts @@ -1,5 +1,9 @@ -import type { LanguageServicePlugin } from '@volar/language-service'; -import { create as baseCreate } from 'volar-service-css'; +import type { LanguageServicePlugin, VirtualCode } from '@volar/language-service'; +import { VueVirtualCode } from '@vue/language-core'; +import { create as baseCreate, type Provide } from 'volar-service-css'; +import * as css from 'vscode-css-languageservice'; +import type { TextDocument } from 'vscode-languageserver-textdocument'; +import { URI } from 'vscode-uri'; export function create(): LanguageServicePlugin { const base = baseCreate({ scssDocumentSelector: ['scss', 'postcss'] }); @@ -7,6 +11,11 @@ export function create(): LanguageServicePlugin { ...base, create(context) { const baseInstance = base.create(context); + const { + 'css/languageService': getCssLs, + 'css/stylesheet': getStylesheet, + } = baseInstance.provide as Provide; + return { ...baseInstance, async provideDiagnostics(document, token) { @@ -18,7 +27,73 @@ export function create(): LanguageServicePlugin { } return diagnostics; }, + /** + * If the editing position is within the virtual code and navigation is enabled, + * skip the CSS renaming feature. + */ + provideRenameRange(document, position) { + do { + const uri = URI.parse(document.uri); + const decoded = context.decodeEmbeddedDocumentUri(uri); + const sourceScript = decoded && context.language.scripts.get(decoded[0]); + const virtualCode = decoded && sourceScript?.generated?.embeddedCodes.get(decoded[1]); + if (!sourceScript?.generated || !virtualCode?.id.startsWith('style_')) { + break; + } + + const root = sourceScript.generated.root; + if (!(root instanceof VueVirtualCode)) { + break; + } + + const block = root.sfc.styles.find(style => style.name === decoded![1]); + if (!block) { + break; + } + + let script: VirtualCode | undefined; + for (const [key, value] of sourceScript.generated.embeddedCodes) { + if (key.startsWith('script_')) { + script = value; + break; + } + } + if (!script) { + break; + } + + const offset = document.offsetAt(position) + block.startTagEnd; + for (const { sourceOffsets, lengths, data } of script.mappings) { + if ( + !sourceOffsets.length + || !data.navigation + || typeof data.navigation === 'object' && !data.navigation.shouldRename + ) { + continue; + } + + const start = sourceOffsets[0]; + const end = sourceOffsets.at(-1)! + lengths.at(-1)!; + + if (offset >= start && offset <= end) { + return; + } + } + } while (0); + + return worker(document, (stylesheet, cssLs) => { + return cssLs.prepareRename(document, position, stylesheet); + }); + } }; + + 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 6bab161e7c..77723ae977 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 919e0c3c27..d7cc7df4c7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -244,6 +244,9 @@ importers: volar-service-typescript-twoslash-queries: specifier: 0.0.62 version: 0.0.62(@volar/language-service@2.4.11) + vscode-css-languageservice: + specifier: ^6.3.1 + version: 6.3.2 vscode-html-languageservice: specifier: ^5.2.0 version: 5.3.1 @@ -1016,51 +1019,61 @@ packages: resolution: {integrity: sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.34.6': resolution: {integrity: sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.34.6': resolution: {integrity: sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.34.6': resolution: {integrity: sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loongarch64-gnu@4.34.6': resolution: {integrity: sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-powerpc64le-gnu@4.34.6': resolution: {integrity: sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-gnu@4.34.6': resolution: {integrity: sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-s390x-gnu@4.34.6': resolution: {integrity: sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.34.6': resolution: {integrity: sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.34.6': resolution: {integrity: sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-win32-arm64-msvc@4.34.6': resolution: {integrity: sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==}