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 { "line": 5, "offset": 20, }, + "suffixText": ": foo", }, { "end": { @@ -1239,6 +1240,178 @@ test('Template Ref', async () => { `); }); +test('Same Name Shorthand', async () => { + expect( + await requestRenameToTsServer( + 'tsconfigProject/fixture.vue', + 'vue', + ` + + + + `, + ), + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "fooBar", + "fullDisplayName": "__object.fooBar", + "kind": "property", + "kindModifiers": "", + "triggerSpan": { + "end": { + "line": 3, + "offset": 19, + }, + "start": { + "line": 3, + "offset": 12, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "contextEnd": { + "line": 8, + "offset": 21, + }, + "contextStart": { + "line": 8, + "offset": 4, + }, + "end": { + "line": 8, + "offset": 16, + }, + "start": { + "line": 8, + "offset": 10, + }, + }, + { + "end": { + "line": 4, + "offset": 25, + }, + "prefixText": "fooBar: ", + "start": { + "line": 4, + "offset": 19, + }, + }, + { + "end": { + "line": 3, + "offset": 19, + }, + "prefixText": "foo-bar="", + "start": { + "line": 3, + "offset": 12, + }, + "suffixText": """, + }, + ], + }, + ], + } + `); + expect( + await requestRenameToTsServer( + 'tsconfigProject/fixture.vue', + 'vue', + ` + + + + `, + ), + ).toMatchInlineSnapshot(` + { + "info": { + "canRename": true, + "displayName": "fooBar", + "fullDisplayName": "__type.fooBar", + "kind": "property", + "kindModifiers": "declare", + "triggerSpan": { + "end": { + "line": 9, + "offset": 11, + }, + "start": { + "line": 9, + "offset": 5, + }, + }, + }, + "locs": [ + { + "file": "\${testWorkspacePath}/tsconfigProject/fixture.vue", + "locs": [ + { + "end": { + "line": 4, + "offset": 25, + }, + "start": { + "line": 4, + "offset": 19, + }, + "suffixText": ": fooBar", + }, + { + "end": { + "line": 3, + "offset": 19, + }, + "start": { + "line": 3, + "offset": 12, + }, + "suffixText": "="fooBar"", + }, + { + "contextEnd": { + "line": 9, + "offset": 20, + }, + "contextStart": { + "line": 9, + "offset": 5, + }, + "end": { + "line": 9, + "offset": 11, + }, + "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 711fad2dcc..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; @@ -20,6 +20,7 @@ export function preprocessLanguageService( getSuggestionDiagnostics, getCompletionsAtPosition, getCodeFixesAtPosition, + findRenameLocations, } = languageService; languageService.getQuickInfoAtPosition = (fileName, position, ...rests) => { @@ -200,6 +201,117 @@ 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; + + // { 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, () => true)) { + for (const _ of map.toGeneratedLocation(offset, isShorthand)) { + return; + } + } + } + + const preferAlias = typeof rests[2] === 'boolean' + ? rests[2] + : rests[2]?.providePrefixAndSuffixTextForRename ?? true; + if (!preferAlias) { + return result; + } + + 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(generatedLeft, generatedRight, 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(generatedLeft, generatedRight, 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: `="${camelize(originalText)}"`, + }; + } + else { + return { + prefixText: `${originalText}="`, + suffixText: `"`, + }; + } + } + else { + if (target === 'left') { + return { + suffixText: `: ${originalText}`, + }; + } + else { + return { + prefixText: `${originalText}: `, + }; + } + } } export function postprocessLanguageService(