diff --git a/packages/language-service/lib/documents.ts b/packages/language-service/lib/documents.ts index 6bede338..5847a99d 100644 --- a/packages/language-service/lib/documents.ts +++ b/packages/language-service/lib/documents.ts @@ -26,13 +26,13 @@ export class SourceMapWithDocuments { } public * getSourceRanges(range: vscode.Range, filter: (data: CodeInformation) => boolean = () => true) { - for (const result of this.findRanges(range, filter, 'getSourcePositionsBase', 'matchSourcePosition')) { + for (const result of this.findRanges(range, filter, this.embeddedDocument, this.sourceDocument, 'getSourceStartEnd', 'getSourcePositions')) { yield result; } } public * getGeneratedRanges(range: vscode.Range, filter: (data: CodeInformation) => boolean = () => true) { - for (const result of this.findRanges(range, filter, 'getGeneratedPositionsBase', 'matchGeneratedPosition')) { + for (const result of this.findRanges(range, filter, this.sourceDocument, this.embeddedDocument, 'getGeneratedStartEnd', 'getGeneratedPositions')) { yield result; } } @@ -40,61 +40,34 @@ export class SourceMapWithDocuments { protected * findRanges( range: vscode.Range, filter: (data: CodeInformation) => boolean, - api: 'getSourcePositionsBase' | 'getGeneratedPositionsBase', - api2: 'matchSourcePosition' | 'matchGeneratedPosition' + fromDoc: TextDocument, + toDoc: TextDocument, + api: 'getSourceStartEnd' | 'getGeneratedStartEnd', + api2: 'getSourcePositions' | 'getGeneratedPositions' ) { - const failedLookUps: (readonly [vscode.Position, Mapping])[] = []; - for (const mapped of this[api](range.start, filter)) { - const end = this[api2](range.end, mapped[1]); - if (end) { - yield { start: mapped[0], end } as vscode.Range; - } - else { - failedLookUps.push(mapped); + for (const [mappedStart, mappedEnd, mapping] of this.map[api](fromDoc.offsetAt(range.start), fromDoc.offsetAt(range.end))) { + if (!filter(mapping.data)) { + continue; } + yield { start: toDoc.positionAt(mappedStart), end: toDoc.positionAt(mappedEnd) }; } - for (const failedLookUp of failedLookUps) { - for (const mapped of this[api](range.end, filter)) { - yield { start: failedLookUp[0], end: mapped[0] } as vscode.Range; + for (const mappedStart of this[api2](range.start, filter)) { + for (const mappedEnd of this[api2](range.end, filter)) { + yield { start: mappedStart, end: mappedEnd }; + break; } } } - // Position APIs - - public getSourcePosition(position: vscode.Position, filter: (data: CodeInformation) => boolean = () => true) { - for (const mapped of this.getSourcePositions(position, filter)) { - return mapped; - } - } - - public getGeneratedPosition(position: vscode.Position, filter: (data: CodeInformation) => boolean = () => true) { - for (const mapped of this.getGeneratedPositions(position, filter)) { - return mapped; - } - } - public * getSourcePositions(position: vscode.Position, filter: (data: CodeInformation) => boolean = () => true) { - for (const mapped of this.getSourcePositionsBase(position, filter)) { + for (const mapped of this.findPositions(position, filter, this.embeddedDocument, this.sourceDocument, 'generatedOffsets', 'sourceOffsets')) { yield mapped[0]; } } public * getGeneratedPositions(position: vscode.Position, filter: (data: CodeInformation) => boolean = () => true) { - for (const mapped of this.getGeneratedPositionsBase(position, filter)) { - yield mapped[0]; - } - } - - public * getSourcePositionsBase(position: vscode.Position, filter: (data: CodeInformation) => boolean = () => true) { - for (const mapped of this.findPositions(position, filter, this.embeddedDocument, this.sourceDocument, 'generatedOffsets', 'sourceOffsets')) { - yield mapped; - } - } - - public * getGeneratedPositionsBase(position: vscode.Position, filter: (data: CodeInformation) => boolean = () => true) { for (const mapped of this.findPositions(position, filter, this.sourceDocument, this.embeddedDocument, 'sourceOffsets', 'generatedOffsets')) { - yield mapped; + yield mapped[0]; } } diff --git a/packages/language-service/lib/features/provideDocumentFormattingEdits.ts b/packages/language-service/lib/features/provideDocumentFormattingEdits.ts index 8b4df95d..53080f30 100644 --- a/packages/language-service/lib/features/provideDocumentFormattingEdits.ts +++ b/packages/language-service/lib/features/provideDocumentFormattingEdits.ts @@ -98,9 +98,7 @@ export function register(context: LanguageServiceContext) { if (onTypeParams) { - const embeddedPosition = docMap.getGeneratedPosition(onTypeParams.position); - - if (embeddedPosition) { + for (const embeddedPosition of docMap.getGeneratedPositions(onTypeParams.position)) { embeddedCodeResult = await tryFormat( docMap.sourceDocument, docMap.embeddedDocument, @@ -110,6 +108,7 @@ export function register(context: LanguageServiceContext) { embeddedPosition, onTypeParams.ch ); + break; } } else if (embeddedRange) { diff --git a/packages/language-service/lib/features/provideInlayHints.ts b/packages/language-service/lib/features/provideInlayHints.ts index e5dc7db4..66fed21a 100644 --- a/packages/language-service/lib/features/provideInlayHints.ts +++ b/packages/language-service/lib/features/provideInlayHints.ts @@ -62,12 +62,11 @@ export function register(context: LanguageServiceContext) { } return inlayHints .map((_inlayHint): vscode.InlayHint | undefined => { - const position = map.getSourcePosition(_inlayHint.position, isInlayHintsEnabled); const edits = _inlayHint.textEdits ?.map(textEdit => transformTextEdit(textEdit, range => map.getSourceRange(range), map.embeddedDocument)) .filter(notEmpty); - if (position) { + for (const position of map.getSourcePositions(_inlayHint.position, isInlayHintsEnabled)) { return { ..._inlayHint, position, diff --git a/packages/language-service/lib/features/provideSelectionRanges.ts b/packages/language-service/lib/features/provideSelectionRanges.ts index 71e9185f..ce4cc07f 100644 --- a/packages/language-service/lib/features/provideSelectionRanges.ts +++ b/packages/language-service/lib/features/provideSelectionRanges.ts @@ -17,7 +17,11 @@ export function register(context: LanguageServiceContext) { () => positions, function* (map) { const result = positions - .map(position => map.getGeneratedPosition(position, isSelectionRangesEnabled)) + .map(position => { + for (const mappedPosition of map.getGeneratedPositions(position, isSelectionRangesEnabled)) { + return mappedPosition; + } + }) .filter(notEmpty); if (result.length) { yield result; diff --git a/packages/language-service/lib/utils/transform.ts b/packages/language-service/lib/utils/transform.ts index cb8244ad..899b4df4 100644 --- a/packages/language-service/lib/utils/transform.ts +++ b/packages/language-service/lib/utils/transform.ts @@ -42,13 +42,17 @@ export function transformDocumentLinkTarget(_target: string, context: LanguageSe } } else { - const sourcePos = map.getSourcePosition({ line: startLine, character: startCharacter }); - if (sourcePos) { + let mapped = false; + for (const sourcePos of map.getSourcePositions({ line: startLine, character: startCharacter })) { + mapped = true; target = target.with({ fragment: 'L' + (sourcePos.line + 1) + ',' + (sourcePos.character + 1), }); break; } + if (mapped) { + break; + } } } } diff --git a/packages/source-map/lib/sourceMap.ts b/packages/source-map/lib/sourceMap.ts index 8512af96..16ab7742 100644 --- a/packages/source-map/lib/sourceMap.ts +++ b/packages/source-map/lib/sourceMap.ts @@ -23,6 +23,23 @@ export class SourceMap { constructor(public readonly mappings: Mapping[]) { } + getSourceStartEnd(generatedStart: number, generatedEnd: number) { + return this.findMatchingStartEnd(generatedStart, generatedEnd, 'generatedOffsets', 'sourceOffsets'); + } + + getGeneratedStartEnd(sourceStart: number, sourceEnd: number) { + return this.findMatchingStartEnd(sourceStart, sourceEnd, 'sourceOffsets', 'generatedOffsets'); + } + + * findMatchingStartEnd(start: number, end: number, fromRange: CodeRangeKey, toRange: CodeRangeKey) { + for (const [mappedStart, mapping] of this.findMatching(start, fromRange, toRange)) { + const mappedEnd = translateOffset(end, mapping[fromRange], mapping[toRange], getLengths(mapping, fromRange), getLengths(mapping, toRange)); + if (mappedEnd !== undefined) { + yield [mappedStart, mappedEnd, mapping] as const; + } + }; + } + getSourceOffsets(generatedOffset: number) { return this.findMatching(generatedOffset, 'generatedOffsets', 'sourceOffsets'); } diff --git a/packages/source-map/tests/sourceMap.spec.ts b/packages/source-map/tests/sourceMap.spec.ts new file mode 100644 index 00000000..c1f68e02 --- /dev/null +++ b/packages/source-map/tests/sourceMap.spec.ts @@ -0,0 +1,162 @@ +import { describe, expect, test } from 'vitest'; +import { SourceMap } from '../lib/sourceMap'; + +describe('sourceMap', () => { + test('Angular template', () => { + const map = new SourceMap([ + { + sourceOffsets: [ + `{{|data?.icon?.toString()}}`.indexOf('|'), + // ^^^^ + ], + generatedOffsets: [ + `(null as any ? ((null as any ? ((null as any ? (this.|data)!.icon : undefined)!.toString : undefined))!() : undefined)`.indexOf('|'), + // ^^^^ + ], + lengths: [ + `data`.length, + ], + data: {}, + }, + { + sourceOffsets: [ + `{{data?.|icon?.toString()}}`.indexOf('|'), + // ^^^^ + ], + generatedOffsets: [ + `(null as any ? ((null as any ? ((null as any ? (this.data)!.|icon : undefined)!.toString : undefined))!() : undefined)`.indexOf('|'), + // ^^^^ + ], + lengths: [ + `icon`.length, + ], + data: {}, + }, + { + sourceOffsets: [ + `{{data?.icon?.|toString()}}`.indexOf('|'), + // ^^^^^^^^ + ], + generatedOffsets: [ + `(null as any ? ((null as any ? ((null as any ? (this.data)!.icon : undefined)!.|toString : undefined))!() : undefined)`.indexOf('|'), + // ^^^^^^^^ + ], + lengths: [ + `toString`.length, + ], + data: {}, + }, + { + sourceOffsets: [ + `{{data?.icon?.|toString()}}`.indexOf('|'), + // ^^ + ], + generatedOffsets: [ + `(null as any ? ((null as any ? ((null as any ? (this.data)!.icon : undefined)!.toString : undefined))!|() : undefined)`.indexOf('|'), + // ^^ + ], + lengths: [ + `()`.length, + ], + data: {}, + }, + { + sourceOffsets: [ + `{{|data?.icon?.toString()}}`.indexOf('|'), + // ^ + `{{data?.icon|?.toString()}}`.indexOf('|'), + // ^ + ], + generatedOffsets: [ + `(null as any ? ((null as any ? (|(null as any ? (this.data)!.icon : undefined)!.toString : undefined))!() : undefined)`.indexOf('|'), + // ^ + `(null as any ? ((null as any ? ((null as any ? (this.data)!.icon : undefined)|!.toString : undefined))!() : undefined)`.indexOf('|'), + // ^ + ], + lengths: [0, 0], + data: {}, + }, + { + sourceOffsets: [ + `{{|data?.icon?.toString()}}`.indexOf('|'), + // ^ + `{{data?.icon?.toString|()}}`.indexOf('|'), + // ^ + ], + generatedOffsets: [ + `(null as any ? (|(null as any ? ((null as any ? (this.data)!.icon : undefined)!.toString : undefined))!() : undefined)`.indexOf('|'), + // ^ + `(null as any ? ((null as any ? ((null as any ? (this.data)!.icon : undefined)!.toString : undefined))|!() : undefined)`.indexOf('|'), + // ^ + ], + lengths: [0, 0], + data: {}, + }, + { + sourceOffsets: [ + `{{|data?.icon?.toString()}}`.indexOf('|'), + // ^ + `{{data?.icon?.toString()|}}`.indexOf('|'), + // ^ + ], + generatedOffsets: [ + 0, + `(null as any ? ((null as any ? ((null as any ? (this.data)!.icon : undefined)!.toString : undefined))!() : undefined)`.length, + ], + lengths: [0, 0], + data: {}, + }, + ]); + + expect([...map.getGeneratedStartEnd( + `{{|data?.icon?.toString()}}`.indexOf('|'), + `{{data|?.icon?.toString()}}`.indexOf('|') + )].map(mapped => mapped.slice(0, 2))).toEqual([ + [ + `(null as any ? ((null as any ? ((null as any ? (this.|data)!.icon : undefined)!.toString : undefined))!() : undefined)`.indexOf('|'), + // ^ + `(null as any ? ((null as any ? ((null as any ? (this.data|)!.icon : undefined)!.toString : undefined))!() : undefined)`.indexOf('|'), + // ^ + ], + ]); + + expect([...map.getGeneratedStartEnd( + `{{|data?.icon?.toString()}}`.indexOf('|'), + `{{data?.ic|on?.toString()}}`.indexOf('|') + )].map(mapped => mapped.slice(0, 2))).toEqual([]); + + expect([...map.getGeneratedStartEnd( + `{{|data?.icon?.toString()}}`.indexOf('|'), + `{{data?.icon|?.toString()}}`.indexOf('|') + )].map(mapped => mapped.slice(0, 2))).toEqual([ + [ + `(null as any ? ((null as any ? (|(null as any ? (this.data)!.icon : undefined)!.toString : undefined))!() : undefined)`.indexOf('|'), + // ^ + `(null as any ? ((null as any ? ((null as any ? (this.data)!.icon : undefined)|!.toString : undefined))!() : undefined)`.indexOf('|'), + // ^ + ], + ]); + + expect([...map.getGeneratedStartEnd( + `{{|data?.icon?.toString()}}`.indexOf('|'), + `{{data?.icon?.toString|()}}`.indexOf('|') + )].map(mapped => mapped.slice(0, 2))).toEqual([ + [ + `(null as any ? (|(null as any ? ((null as any ? (this.data)!.icon : undefined)!.toString : undefined))!() : undefined)`.indexOf('|'), + // ^ + `(null as any ? ((null as any ? ((null as any ? (this.data)!.icon : undefined)!.toString : undefined))|!() : undefined)`.indexOf('|'), + // ^ + ], + ]); + + expect([...map.getGeneratedStartEnd( + `{{|data?.icon?.toString()}}`.indexOf('|'), + `{{data?.icon?.toString()|}}`.indexOf('|') + )].map(mapped => mapped.slice(0, 2))).toEqual([ + [ + 0, + `(null as any ? ((null as any ? ((null as any ? (this.data)!.icon : undefined)!.toString : undefined))!() : undefined)`.length, + ], + ]); + }); +}); diff --git a/packages/typescript/lib/node/decorateLanguageService.ts b/packages/typescript/lib/node/decorateLanguageService.ts index bcb5fddc..bbc69a83 100644 --- a/packages/typescript/lib/node/decorateLanguageService.ts +++ b/packages/typescript/lib/node/decorateLanguageService.ts @@ -23,8 +23,9 @@ import { getMappingOffset, toGeneratedOffset, toGeneratedOffsets, + toGeneratedRanges, toSourceOffset, - toSourceOffsets, + toSourceRanges, transformCallHierarchyItem, transformDiagnostic, transformDocumentSpan, @@ -413,15 +414,16 @@ export function decorateLanguageService( return []; } if (serviceScript) { - const generatePosition = toGeneratedOffset(language, serviceScript, sourceScript, typeof positionOrRange === 'number' ? positionOrRange : positionOrRange.pos, isCodeActionsEnabled); - if (generatePosition !== undefined) { - const por = typeof positionOrRange === 'number' - ? generatePosition - : { - pos: generatePosition, - end: generatePosition + positionOrRange.end - positionOrRange.pos, - }; - return getApplicableRefactors(targetScript.id, por, preferences, triggerReason, kind, includeInteractiveActions); + if (typeof positionOrRange === 'number') { + const generatePosition = toGeneratedOffset(language, serviceScript, sourceScript, positionOrRange, isCodeActionsEnabled); + if (generatePosition !== undefined) { + return getApplicableRefactors(targetScript.id, generatePosition, preferences, triggerReason, kind, includeInteractiveActions); + } + } + else { + for (const [generatedStart, generatedEnd] of toGeneratedRanges(language, serviceScript, sourceScript, positionOrRange.pos, positionOrRange.end, isCodeActionsEnabled)) { + return getApplicableRefactors(targetScript.id, { pos: generatedStart, end: generatedEnd }, preferences, triggerReason, kind, includeInteractiveActions); + } } return []; } @@ -437,23 +439,16 @@ export function decorateLanguageService( return undefined; } if (serviceScript) { - const generatePosition = toGeneratedOffset( - language, - serviceScript, - sourceScript, - typeof positionOrRange === 'number' - ? positionOrRange - : positionOrRange.pos, - isCodeActionsEnabled - ); - if (generatePosition !== undefined) { - const por = typeof positionOrRange === 'number' - ? generatePosition - : { - pos: generatePosition, - end: generatePosition + positionOrRange.end - positionOrRange.pos, - }; - edits = getEditsForRefactor(targetScript.id, formatOptions, por, refactorName, actionName, preferences); + if (typeof positionOrRange === 'number') { + const generatePosition = toGeneratedOffset(language, serviceScript, sourceScript, positionOrRange, isCodeActionsEnabled); + if (generatePosition !== undefined) { + edits = getEditsForRefactor(targetScript.id, formatOptions, generatePosition, refactorName, actionName, preferences); + } + } + else { + for (const [generatedStart, generatedEnd] of toGeneratedRanges(language, serviceScript, sourceScript, positionOrRange.pos, positionOrRange.end, isCodeActionsEnabled)) { + edits = getEditsForRefactor(targetScript.id, formatOptions, { pos: generatedStart, end: generatedEnd }, refactorName, actionName, preferences); + } } } else { @@ -560,20 +555,13 @@ export function decorateLanguageService( const result = getEncodedSemanticClassifications(targetScript.id, { start, length: end - start }, format); const spans: number[] = []; for (let i = 0; i < result.spans.length; i += 3) { - for (const sourceStart of toSourceOffsets(sourceScript, language, serviceScript, result.spans[i], isSemanticTokensEnabled)) { - for (const sourceEnd of toSourceOffsets(sourceScript, language, serviceScript, result.spans[i] + result.spans[i + 1], isSemanticTokensEnabled)) { - if (sourceStart[0] === sourceEnd[0] && sourceEnd[1] >= sourceStart[1]) { - spans.push( - sourceStart[1], - sourceEnd[1] - sourceStart[1], - result.spans[i + 2] - ); - break; - } - } - if (spans.length) { - break; - } + for (const [_, sourceStart, sourceEnd] of toSourceRanges(sourceScript, language, serviceScript, result.spans[i], result.spans[i] + result.spans[i + 1], isSemanticTokensEnabled)) { + spans.push( + sourceStart, + sourceEnd - sourceStart, + result.spans[i + 2] + ); + break; } } result.spans = spans; diff --git a/packages/typescript/lib/node/transform.ts b/packages/typescript/lib/node/transform.ts index cdd07479..d20d52a8 100644 --- a/packages/typescript/lib/node/transform.ts +++ b/packages/typescript/lib/node/transform.ts @@ -215,18 +215,11 @@ export function transformTextSpan( ): [string, ts.TextSpan] | undefined { const start = textSpan.start; const end = textSpan.start + textSpan.length; - for (const sourceStart of toSourceOffsets(sourceScript, language, serviceScript, start, filter)) { - for (const sourceEnd of toSourceOffsets(sourceScript, language, serviceScript, end, filter)) { - if ( - sourceStart[0] === sourceEnd[0] - && sourceEnd[1] >= sourceStart[1] - ) { - return [sourceStart[0], { - start: sourceStart[1], - length: sourceEnd[1] - sourceStart[1], - }]; - } - } + for (const [fileName, sourceStart, sourceEnd] of toSourceRanges(sourceScript, language, serviceScript, start, end, filter)) { + return [fileName, { + start: sourceStart, + length: sourceEnd - sourceStart, + }]; } } @@ -242,6 +235,49 @@ export function toSourceOffset( } } +export function* toSourceRanges( + sourceScript: SourceScript | undefined, + language: Language, + serviceScript: TypeScriptServiceScript, + start: number, + end: number, + filter: (data: CodeInformation) => boolean +): Generator<[fileName: string, start: number, end: number]> { + if (sourceScript) { + const map = language.maps.get(serviceScript.code, sourceScript); + let mapped = false; + for (const [sourceStart, sourceEnd, mapping] of map.getSourceStartEnd(start - getMappingOffset(language, serviceScript), end - getMappingOffset(language, serviceScript))) { + if (filter(mapping.data)) { + mapped = true; + yield [sourceScript.id, sourceStart, sourceEnd]; + } + } + if (!mapped) { + // fallback + for (const sourceStart of toSourceOffsets(sourceScript, language, serviceScript, start, filter)) { + for (const sourceEnd of toSourceOffsets(sourceScript, language, serviceScript, end, filter)) { + if ( + sourceStart[0] === sourceEnd[0] + && sourceEnd[1] >= sourceStart[1] + ) { + yield [sourceStart[0], sourceStart[1], sourceEnd[1]]; + break; + } + } + } + } + } + else { + for (const [fileName, _snapshot, map] of language.maps.forEach(serviceScript.code)) { + for (const [sourceStart, sourceEnd, mapping] of map.getSourceStartEnd(start - getMappingOffset(language, serviceScript), end - getMappingOffset(language, serviceScript))) { + if (filter(mapping.data)) { + yield [fileName, sourceStart, sourceEnd]; + } + } + } + } +} + export function* toSourceOffsets( sourceScript: SourceScript | undefined, language: Language, @@ -268,6 +304,38 @@ export function* toSourceOffsets( } } +export function* toGeneratedRanges( + language: Language, + serviceScript: TypeScriptServiceScript, + sourceScript: SourceScript, + start: number, + end: number, + filter: (data: CodeInformation) => boolean +) { + const map = language.maps.get(serviceScript.code, sourceScript); + let mapped = false; + for (const [generateStart, generateEnd, mapping] of map.getGeneratedStartEnd(start, end)) { + if (filter(mapping.data)) { + mapped = true; + yield [ + generateStart + getMappingOffset(language, serviceScript), + generateEnd + getMappingOffset(language, serviceScript), + ] as const; + } + } + if (!mapped) { + // fallback + for (const [generatedStart] of toGeneratedOffsets(language, serviceScript, sourceScript, start, filter)) { + for (const [generatedEnd] of toGeneratedOffsets(language, serviceScript, sourceScript, end, filter)) { + if (generatedEnd >= generatedStart) { + yield [generatedStart, generatedEnd] as const; + break; + } + } + } + } +} + export function toGeneratedOffset( language: Language, serviceScript: TypeScriptServiceScript,