Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(source-map): improve range mapping accuracy #204

Merged
merged 3 commits into from
Jun 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 16 additions & 43 deletions packages/language-service/lib/documents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,75 +26,48 @@ 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;
}
}

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<CodeInformation>])[] = [];
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];
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -110,6 +108,7 @@ export function register(context: LanguageServiceContext) {
embeddedPosition,
onTypeParams.ch
);
break;
}
}
else if (embeddedRange) {
Expand Down
3 changes: 1 addition & 2 deletions packages/language-service/lib/features/provideInlayHints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
8 changes: 6 additions & 2 deletions packages/language-service/lib/utils/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions packages/source-map/lib/sourceMap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ export class SourceMap<Data = unknown> {

constructor(public readonly mappings: Mapping<Data>[]) { }

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');
}
Expand Down
162 changes: 162 additions & 0 deletions packages/source-map/tests/sourceMap.spec.ts
Original file line number Diff line number Diff line change
@@ -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,
],
]);
});
});
Loading
Loading