From 587d50141dc7247955864b76722906dc0deb1302 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Thu, 25 Dec 2025 02:45:56 +0800 Subject: [PATCH 1/6] feat: correct rename behavior on same name shorthands in template --- .../lib/codegen/template/elementProps.ts | 5 +- .../lib/codegen/template/interpolation.ts | 6 +- packages/language-core/lib/types.ts | 1 + packages/typescript-plugin/lib/common.ts | 123 ++++++++++++++++++ 4 files changed, 132 insertions(+), 3 deletions(-) diff --git a/packages/language-core/lib/codegen/template/elementProps.ts b/packages/language-core/lib/codegen/template/elementProps.ts index 0a28cbd877..08a2bbb5b4 100644 --- a/packages/language-core/lib/codegen/template/elementProps.ts +++ b/packages/language-core/lib/codegen/template/elementProps.ts @@ -252,7 +252,10 @@ export function* generatePropExp( exp.loc.source, 'template', exp.loc.start.offset, - codeFeatures.withoutHighlightAndCompletion, + { + ...codeFeatures.withoutHighlightAndCompletion, + __shorthandExpression: 'html', + }, ); if (ctx.scopes.some(scope => scope.has(propVariableName))) { diff --git a/packages/language-core/lib/codegen/template/interpolation.ts b/packages/language-core/lib/codegen/template/interpolation.ts index ed0202ee4a..378229bb62 100644 --- a/packages/language-core/lib/codegen/template/interpolation.ts +++ b/packages/language-core/lib/codegen/template/interpolation.ts @@ -61,6 +61,8 @@ export function* generateInterpolation( start + offset, type === 'errorMappingOnly' ? codeFeatures.verification + : type === 'shorthand' + ? { ...data, __shorthandExpression: 'js' } : data, ]; } @@ -81,7 +83,7 @@ function* forEachInterpolationSegment( [ code: string, offset: number, - type?: 'errorMappingOnly' | 'startEnd', + type?: 'errorMappingOnly' | 'shorthand' | 'startEnd', ] | string > { const code = prefix + originalCode + suffix; @@ -120,7 +122,7 @@ function* forEachInterpolationSegment( yield names.ctx; } yield `.`; - yield [name, offset]; + yield [name, offset, isShorthand ? 'shorthand' : undefined]; } prevEnd = offset + name.length; diff --git a/packages/language-core/lib/types.ts b/packages/language-core/lib/types.ts index 787e279f46..1bfacd6fc1 100644 --- a/packages/language-core/lib/types.ts +++ b/packages/language-core/lib/types.ts @@ -17,6 +17,7 @@ export type RawVueCompilerOptions = Partial { @@ -200,6 +201,128 @@ export function preprocessLanguageService( } return result; }; + + languageService.findRenameLocations = (fileName, position, ...rests) => { + // @ts-expect-error + const result = findRenameLocations(fileName, position, ...rests); + if (!result?.length) { + return result; + } + + const language = getLanguage(); + if (!language) { + return result; + } + + const [serviceScript, _targetScript, sourceScript] = getServiceScript(language, fileName); + if (!serviceScript || !(sourceScript?.generated?.root instanceof VueVirtualCode)) { + return result; + } + + const map = language.maps.get(serviceScript.code, sourceScript); + const leadingOffset = sourceScript.snapshot.getLength(); + const isShorthand = (data: VueCodeInformation) => !!data.__shorthandExpression; + const isNotShorthand = (data: VueCodeInformation) => !data.__shorthandExpression; + + // { foo: __VLS_ctx.foo } + // ^^^ ^^^ + // if the rename is triggered directly on the shorthand, + // skip the entire request on the generated property name + if ([...map.toSourceLocation(position - leadingOffset, isShorthand)].length === 0) { + for (const [offset] of map.toSourceLocation(position - leadingOffset, isNotShorthand)) { + for (const _ of map.toGeneratedLocation(offset, isShorthand)) { + return; + } + } + } + + const preferAlias = typeof rests[2] === 'boolean' ? rests[2] : rests[2]?.providePrefixAndSuffixTextForRename; + if (!preferAlias) { + return result; + } + + const locations = [...result]; + outer: for (let i = 0; i < locations.length; i++) { + const { textSpan } = locations[i]!; + + // { foo: __VLS_ctx.foo } + // ^^^ + for ( + const [start, end, { data }] of map.toSourceRange( + textSpan.start - leadingOffset, + textSpan.start + textSpan.length - leadingOffset, + true, + isShorthand, + ) + ) { + locations.splice(i, 1, { + ...locations[i]!, + ...getPrefixAndSuffixForShorthandRename( + (data as VueCodeInformation).__shorthandExpression!, + 'right', + sourceScript.snapshot.getText(start, end), + ), + }); + continue outer; + } + + // { foo: __VLS_ctx.foo } + // ^^^ + for ( + const [start, end] of map.toSourceRange( + textSpan.start - leadingOffset, + textSpan.start + textSpan.length - leadingOffset, + true, + () => true, + ) + ) { + for (const [, , { data }] of map.toGeneratedRange(start, end, true, isShorthand)) { + locations.splice(i, 1, { + ...locations[i]!, + ...getPrefixAndSuffixForShorthandRename( + (data as VueCodeInformation).__shorthandExpression!, + 'left', + sourceScript.snapshot.getText(start, end), + ), + }); + continue outer; + } + } + } + return locations; + }; +} + +function getPrefixAndSuffixForShorthandRename( + type: 'html' | 'js', + target: 'left' | 'right', + originalText: string, +): Pick { + if (type === 'html') { + if (target === 'left') { + return { + suffixText: `="${originalText}"`, + }; + } + else { + return { + prefixText: `${originalText}="`, + suffixText: `"`, + }; + } + } + else { + if (target === 'left') { + return { + suffixText: `: ${originalText}`, + }; + } + else { + return { + prefixText: `${originalText}: `, + }; + } + } } export function postprocessLanguageService( From 1c90500be390da0ec9433f08e4e96c5b5eb95ce2 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Thu, 25 Dec 2025 02:59:48 +0800 Subject: [PATCH 2/6] refactor: simplify --- packages/typescript-plugin/lib/common.ts | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index cf5abde5c6..8eddcf0fa3 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -222,14 +222,13 @@ export function preprocessLanguageService( const map = language.maps.get(serviceScript.code, sourceScript); const leadingOffset = sourceScript.snapshot.getLength(); const isShorthand = (data: VueCodeInformation) => !!data.__shorthandExpression; - const isNotShorthand = (data: VueCodeInformation) => !data.__shorthandExpression; // { foo: __VLS_ctx.foo } // ^^^ ^^^ // if the rename is triggered directly on the shorthand, // skip the entire request on the generated property name if ([...map.toSourceLocation(position - leadingOffset, isShorthand)].length === 0) { - for (const [offset] of map.toSourceLocation(position - leadingOffset, isNotShorthand)) { + for (const [offset] of map.toSourceLocation(position - leadingOffset, () => true)) { for (const _ of map.toGeneratedLocation(offset, isShorthand)) { return; } @@ -244,17 +243,12 @@ export function preprocessLanguageService( const locations = [...result]; outer: for (let i = 0; i < locations.length; i++) { const { textSpan } = locations[i]!; + const generatedLeft = textSpan.start - leadingOffset; + const generatedRight = textSpan.start + textSpan.length - leadingOffset; // { foo: __VLS_ctx.foo } // ^^^ - for ( - const [start, end, { data }] of map.toSourceRange( - textSpan.start - leadingOffset, - textSpan.start + textSpan.length - leadingOffset, - true, - isShorthand, - ) - ) { + for (const [start, end, { data }] of map.toSourceRange(generatedLeft, generatedRight, true, isShorthand)) { locations.splice(i, 1, { ...locations[i]!, ...getPrefixAndSuffixForShorthandRename( @@ -268,14 +262,7 @@ export function preprocessLanguageService( // { foo: __VLS_ctx.foo } // ^^^ - for ( - const [start, end] of map.toSourceRange( - textSpan.start - leadingOffset, - textSpan.start + textSpan.length - leadingOffset, - true, - () => true, - ) - ) { + for (const [start, end] of map.toSourceRange(generatedLeft, generatedRight, true, () => true)) { for (const [, , { data }] of map.toGeneratedRange(start, end, true, isShorthand)) { locations.splice(i, 1, { ...locations[i]!, From 531ef2c79f46fef0fa8e21a75059d2f964530bd7 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Thu, 25 Dec 2025 03:11:37 +0800 Subject: [PATCH 3/6] test: add --- .../language-server/tests/renaming.spec.ts | 172 ++++++++++++++++++ packages/typescript-plugin/lib/common.ts | 4 +- 2 files changed, 175 insertions(+), 1 deletion(-) diff --git a/packages/language-server/tests/renaming.spec.ts b/packages/language-server/tests/renaming.spec.ts index c714a46964..fcec750eb1 100644 --- a/packages/language-server/tests/renaming.spec.ts +++ b/packages/language-server/tests/renaming.spec.ts @@ -1239,6 +1239,178 @@ test('Template Ref', async () => { `); }); +test('Same Name Shorthand', async () => { + expect( + await requestRenameToTsServer( + 'tsconfigProject/fixture.vue', + 'vue', + ` + + + + `, + ), + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "foo", + "fullDisplayName": "foo", + "kind": "const", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 8, + "offset": 13, + }, + "start": { + "line": 8, + "offset": 10, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "end": { + "line": 4, + "offset": 22, + }, + "prefixText": "foo: ", + "start": { + "line": 4, + "offset": 19, + }, + }, + { + "end": { + "line": 3, + "offset": 15, + }, + "prefixText": "foo="", + "start": { + "line": 3, + "offset": 12, + }, + "suffixText": """, + }, + { + "contextEnd": { + "line": 8, + "offset": 18, + }, + "contextStart": { + "line": 8, + "offset": 4, + }, + "end": { + "line": 8, + "offset": 13, + }, + "start": { + "line": 8, + "offset": 10, + }, + }, + ], + }, + ], + } + `); + expect( + await requestRenameToTsServer( + 'tsconfigProject/fixture.vue', + 'vue', + ` + + + + `, + ), + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "foo", + "fullDisplayName": "__type.foo", + "kind": "property", + "kindModifiers": "declare", + "triggerSpan": { + "end": { + "line": 9, + "offset": 8, + }, + "start": { + "line": 9, + "offset": 5, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "end": { + "line": 4, + "offset": 22, + }, + "start": { + "line": 4, + "offset": 19, + }, + "suffixText": ": foo", + }, + { + "end": { + "line": 3, + "offset": 15, + }, + "start": { + "line": 3, + "offset": 12, + }, + "suffixText": "="foo"", + }, + { + "contextEnd": { + "line": 9, + "offset": 17, + }, + "contextStart": { + "line": 9, + "offset": 5, + }, + "end": { + "line": 9, + "offset": 8, + }, + "start": { + "line": 9, + "offset": 5, + }, + }, + ], + }, + ], + } + `); +}); + const openedDocuments: TextDocument[] = []; afterEach(async () => { diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index 8eddcf0fa3..82adf28568 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -235,7 +235,9 @@ export function preprocessLanguageService( } } - const preferAlias = typeof rests[2] === 'boolean' ? rests[2] : rests[2]?.providePrefixAndSuffixTextForRename; + const preferAlias = typeof rests[2] === 'boolean' + ? rests[2] + : rests[2]?.providePrefixAndSuffixTextForRename ?? true; if (!preferAlias) { return result; } From 3d849cdba05184767ca5dff932501201223e626a Mon Sep 17 00:00:00 2001 From: KazariEX Date: Thu, 25 Dec 2025 03:14:24 +0800 Subject: [PATCH 4/6] test: update snapshot --- packages/language-server/tests/renaming.spec.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/language-server/tests/renaming.spec.ts b/packages/language-server/tests/renaming.spec.ts index fcec750eb1..7a6eae13ba 100644 --- a/packages/language-server/tests/renaming.spec.ts +++ b/packages/language-server/tests/renaming.spec.ts @@ -1125,6 +1125,7 @@ test('Scoped Classes', async () => { "line": 5, "offset": 20, }, + "suffixText": ": foo", }, { "end": { From 93211b193b1ea310ff4de68a7a8838677e6aa297 Mon Sep 17 00:00:00 2001 From: KazariEX Date: Thu, 25 Dec 2025 03:18:55 +0800 Subject: [PATCH 5/6] test: update --- .../language-server/tests/renaming.spec.ts | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/language-server/tests/renaming.spec.ts b/packages/language-server/tests/renaming.spec.ts index 7a6eae13ba..8b7282361c 100644 --- a/packages/language-server/tests/renaming.spec.ts +++ b/packages/language-server/tests/renaming.spec.ts @@ -1247,12 +1247,12 @@ test('Same Name Shorthand', async () => { 'vue', ` `, ), @@ -1261,17 +1261,17 @@ test('Same Name Shorthand', async () => { "info": { "canRename": true, "displayName": "foo", - "fullDisplayName": "foo", - "kind": "const", + "fullDisplayName": "__object.foo", + "kind": "property", "kindModifiers": "", "triggerSpan": { "end": { - "line": 8, - "offset": 13, + "line": 3, + "offset": 15, }, "start": { - "line": 8, - "offset": 10, + "line": 3, + "offset": 12, }, }, }, @@ -1279,6 +1279,24 @@ test('Same Name Shorthand', async () => { { "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", "locs": [ + { + "contextEnd": { + "line": 8, + "offset": 18, + }, + "contextStart": { + "line": 8, + "offset": 4, + }, + "end": { + "line": 8, + "offset": 13, + }, + "start": { + "line": 8, + "offset": 10, + }, + }, { "end": { "line": 4, @@ -1302,24 +1320,6 @@ test('Same Name Shorthand', async () => { }, "suffixText": """, }, - { - "contextEnd": { - "line": 8, - "offset": 18, - }, - "contextStart": { - "line": 8, - "offset": 4, - }, - "end": { - "line": 8, - "offset": 13, - }, - "start": { - "line": 8, - "offset": 10, - }, - }, ], }, ], From 6209efcdfb041d0658cefe0e893b4ec7e29f03ab Mon Sep 17 00:00:00 2001 From: KazariEX Date: Thu, 25 Dec 2025 03:23:53 +0800 Subject: [PATCH 6/6] fix: camelize `="..."` --- .../language-server/tests/renaming.spec.ts | 48 +++++++++---------- packages/typescript-plugin/lib/common.ts | 4 +- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/packages/language-server/tests/renaming.spec.ts b/packages/language-server/tests/renaming.spec.ts index 8b7282361c..1b897bf1ad 100644 --- a/packages/language-server/tests/renaming.spec.ts +++ b/packages/language-server/tests/renaming.spec.ts @@ -1247,12 +1247,12 @@ test('Same Name Shorthand', async () => { 'vue', ` `, ), @@ -1260,14 +1260,14 @@ test('Same Name Shorthand', async () => { { "info": { "canRename": true, - "displayName": "foo", - "fullDisplayName": "__object.foo", + "displayName": "fooBar", + "fullDisplayName": "__object.fooBar", "kind": "property", "kindModifiers": "", "triggerSpan": { "end": { "line": 3, - "offset": 15, + "offset": 19, }, "start": { "line": 3, @@ -1282,7 +1282,7 @@ test('Same Name Shorthand', async () => { { "contextEnd": { "line": 8, - "offset": 18, + "offset": 21, }, "contextStart": { "line": 8, @@ -1290,7 +1290,7 @@ test('Same Name Shorthand', async () => { }, "end": { "line": 8, - "offset": 13, + "offset": 16, }, "start": { "line": 8, @@ -1300,9 +1300,9 @@ test('Same Name Shorthand', async () => { { "end": { "line": 4, - "offset": 22, + "offset": 25, }, - "prefixText": "foo: ", + "prefixText": "fooBar: ", "start": { "line": 4, "offset": 19, @@ -1311,9 +1311,9 @@ test('Same Name Shorthand', async () => { { "end": { "line": 3, - "offset": 15, + "offset": 19, }, - "prefixText": "foo="", + "prefixText": "foo-bar="", "start": { "line": 3, "offset": 12, @@ -1331,13 +1331,13 @@ test('Same Name Shorthand', async () => { 'vue', ` `, @@ -1346,14 +1346,14 @@ test('Same Name Shorthand', async () => { { "info": { "canRename": true, - "displayName": "foo", - "fullDisplayName": "__type.foo", + "displayName": "fooBar", + "fullDisplayName": "__type.fooBar", "kind": "property", "kindModifiers": "declare", "triggerSpan": { "end": { "line": 9, - "offset": 8, + "offset": 11, }, "start": { "line": 9, @@ -1368,29 +1368,29 @@ test('Same Name Shorthand', async () => { { "end": { "line": 4, - "offset": 22, + "offset": 25, }, "start": { "line": 4, "offset": 19, }, - "suffixText": ": foo", + "suffixText": ": fooBar", }, { "end": { "line": 3, - "offset": 15, + "offset": 19, }, "start": { "line": 3, "offset": 12, }, - "suffixText": "="foo"", + "suffixText": "="fooBar"", }, { "contextEnd": { "line": 9, - "offset": 17, + "offset": 20, }, "contextStart": { "line": 9, @@ -1398,7 +1398,7 @@ test('Same Name Shorthand', async () => { }, "end": { "line": 9, - "offset": 8, + "offset": 11, }, "start": { "line": 9, diff --git a/packages/typescript-plugin/lib/common.ts b/packages/typescript-plugin/lib/common.ts index 82adf28568..57628bd171 100644 --- a/packages/typescript-plugin/lib/common.ts +++ b/packages/typescript-plugin/lib/common.ts @@ -6,7 +6,7 @@ import { } from '@volar/typescript/lib/node/transform'; import { getServiceScript } from '@volar/typescript/lib/node/utils'; import { type Language, type VueCodeInformation, type VueCompilerOptions, VueVirtualCode } from '@vue/language-core'; -import { capitalize, isGloballyAllowed } from '@vue/shared'; +import { camelize, capitalize, isGloballyAllowed } from '@vue/shared'; import type * as ts from 'typescript'; const windowsPathReg = /\\/g; @@ -290,7 +290,7 @@ function getPrefixAndSuffixForShorthandRename( if (type === 'html') { if (target === 'left') { return { - suffixText: `="${originalText}"`, + suffixText: `="${camelize(originalText)}"`, }; } else {