Skip to content

Commit fa1d595

Browse files
authored
fix(typescript): robust calculation of generated span for semantic classifications (#271)
1 parent c3a487d commit fa1d595

File tree

9 files changed

+87
-97
lines changed

9 files changed

+87
-97
lines changed

packages/language-core/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
export { Mapping, SourceMap } from '@volar/source-map';
2-
export * from './lib/editorFeatures';
2+
export * from './lib/editor';
33
export * from './lib/linkedCodeMap';
44
export * from './lib/types';
55
export * from './lib/utils';

packages/language-core/lib/editorFeatures.ts renamed to packages/language-core/lib/editor.ts

Lines changed: 69 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { CodeInformation } from './types';
1+
import type { CodeInformation, Mapper } from './types';
22

33
export function isHoverEnabled(info: CodeInformation): boolean {
44
return !!info.semantic;
@@ -115,3 +115,71 @@ export function shouldReportDiagnostics(info: CodeInformation, source: string |
115115
? info.verification.shouldReport?.(source, code) ?? true
116116
: !!info.verification;
117117
}
118+
119+
export function findOverlapCodeRange(
120+
start: number,
121+
end: number,
122+
map: Mapper,
123+
filter: (data: CodeInformation) => boolean
124+
) {
125+
let mappedStart: number | undefined;
126+
let mappedEnd: number | undefined;
127+
128+
for (const [mapped, mapping] of map.toGeneratedLocation(start)) {
129+
if (filter(mapping.data)) {
130+
mappedStart = mapped;
131+
break;
132+
}
133+
}
134+
for (const [mapped, mapping] of map.toGeneratedLocation(end)) {
135+
if (filter(mapping.data)) {
136+
mappedEnd = mapped;
137+
break;
138+
}
139+
}
140+
141+
if (mappedStart === undefined || mappedEnd === undefined) {
142+
for (const mapping of map.mappings) {
143+
if (filter(mapping.data)) {
144+
const mappingStart = mapping.sourceOffsets[0];
145+
const mappingEnd = mapping.sourceOffsets[mapping.sourceOffsets.length - 1] + mapping.lengths[mapping.lengths.length - 1];
146+
const overlap = getOverlapRange(start, end, mappingStart, mappingEnd);
147+
if (overlap) {
148+
const curMappedStart = (overlap.start - mappingStart) + mapping.generatedOffsets[0];
149+
const lastGeneratedLength = (mapping.generatedLengths ?? mapping.lengths)[mapping.generatedOffsets.length - 1];
150+
const curMappedEndOffset = Math.min(overlap.end - mapping.sourceOffsets[mapping.sourceOffsets.length - 1], lastGeneratedLength);
151+
const curMappedEnd = mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + curMappedEndOffset;
152+
153+
mappedStart = mappedStart === undefined ? curMappedStart : Math.min(mappedStart, curMappedStart);
154+
mappedEnd = mappedEnd === undefined ? curMappedEnd : Math.max(mappedEnd, curMappedEnd);
155+
}
156+
}
157+
}
158+
}
159+
160+
if (mappedStart !== undefined && mappedEnd !== undefined) {
161+
return {
162+
start: mappedStart,
163+
end: mappedEnd,
164+
};
165+
}
166+
}
167+
168+
function getOverlapRange(
169+
range1Start: number,
170+
range1End: number,
171+
range2Start: number,
172+
range2End: number
173+
): { start: number, end: number; } | undefined {
174+
const start = Math.max(range1Start, range2Start);
175+
const end = Math.min(range1End, range2End);
176+
177+
if (start > end) {
178+
return undefined;
179+
}
180+
181+
return {
182+
start,
183+
end,
184+
};
185+
}

packages/language-service/tests/findOverlapCodeRange.spec.ts renamed to packages/language-core/tests/findOverlapCodeRange.spec.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { describe, expect, it } from 'vitest';
2-
import { findOverlapCodeRange } from '../lib/utils/common';
3-
import { CodeInformation, Mapping, defaultMapperFactory } from '@volar/language-core';
2+
import { CodeInformation, Mapping, defaultMapperFactory, findOverlapCodeRange } from '../index';
43

54
// test code: <html><body><p>Hello</p></body></html>
65

packages/language-service/lib/features/provideCodeActions.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { isCodeActionsEnabled } from '@volar/language-core';
1+
import { findOverlapCodeRange, isCodeActionsEnabled } from '@volar/language-core';
22
import type * as vscode from 'vscode-languageserver-protocol';
33
import { URI } from 'vscode-uri';
44
import type { LanguageServiceContext } from '../types';
55
import { NoneCancellationToken } from '../utils/cancellation';
6-
import { findOverlapCodeRange } from '../utils/common';
76
import * as dedupe from '../utils/dedupe';
87
import { getGeneratedRange, languageFeatureWorker } from '../utils/featureWorkers';
98
import { transformLocations, transformWorkspaceEdit } from '../utils/transform';

packages/language-service/lib/features/provideDocumentFormattingEdits.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { SourceScript, VirtualCode, forEachEmbeddedCode, isFormattingEnabled } from '@volar/language-core';
1+
import { SourceScript, VirtualCode, findOverlapCodeRange, forEachEmbeddedCode, isFormattingEnabled } from '@volar/language-core';
22
import type * as vscode from 'vscode-languageserver-protocol';
33
import { TextDocument } from 'vscode-languageserver-textdocument';
44
import { URI } from 'vscode-uri';
55
import type { EmbeddedCodeFormattingOptions, LanguageServiceContext } from '../types';
66
import { NoneCancellationToken } from '../utils/cancellation';
7-
import { findOverlapCodeRange, stringToSnapshot } from '../utils/common';
7+
import { stringToSnapshot } from '../utils/common';
88
import { DocumentsAndMap, getGeneratedPositions, getSourceRange } from '../utils/featureWorkers';
99

1010
export function register(context: LanguageServiceContext) {

packages/language-service/lib/features/provideDocumentSemanticTokens.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import { isSemanticTokensEnabled } from '@volar/language-core';
1+
import { findOverlapCodeRange, isSemanticTokensEnabled } from '@volar/language-core';
22
import type * as vscode from 'vscode-languageserver-protocol';
33
import { URI } from 'vscode-uri';
44
import type { LanguageServiceContext, SemanticToken } from '../types';
55
import { SemanticTokensBuilder } from '../utils/SemanticTokensBuilder';
66
import { NoneCancellationToken } from '../utils/cancellation';
7-
import { findOverlapCodeRange } from '../utils/common';
87
import { getSourceRange, languageFeatureWorker } from '../utils/featureWorkers';
98

109
export function register(context: LanguageServiceContext) {

packages/language-service/lib/features/provideInlayHints.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { isInlayHintsEnabled } from '@volar/language-core';
1+
import { findOverlapCodeRange, isInlayHintsEnabled } from '@volar/language-core';
22
import type * as vscode from 'vscode-languageserver-protocol';
33
import { URI } from 'vscode-uri';
44
import type { LanguageServiceContext } from '../types';
55
import { NoneCancellationToken } from '../utils/cancellation';
6-
import { findOverlapCodeRange } from '../utils/common';
76
import { getSourcePositions, getSourceRange, languageFeatureWorker } from '../utils/featureWorkers';
87
import { transformTextEdit } from '../utils/transform';
98

packages/language-service/lib/utils/common.ts

Lines changed: 0 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -1,76 +1,6 @@
1-
import type { CodeInformation, Mapper } from '@volar/language-core';
21
import type * as ts from 'typescript';
32
import type * as vscode from 'vscode-languageserver-protocol';
43

5-
export function findOverlapCodeRange(
6-
start: number,
7-
end: number,
8-
map: Mapper,
9-
filter: (data: CodeInformation) => boolean
10-
) {
11-
let mappedStart: number | undefined;
12-
let mappedEnd: number | undefined;
13-
14-
for (const [mapped, mapping] of map.toGeneratedLocation(start)) {
15-
if (filter(mapping.data)) {
16-
mappedStart = mapped;
17-
break;
18-
}
19-
}
20-
for (const [mapped, mapping] of map.toGeneratedLocation(end)) {
21-
if (filter(mapping.data)) {
22-
mappedEnd = mapped;
23-
break;
24-
}
25-
}
26-
27-
if (mappedStart === undefined || mappedEnd === undefined) {
28-
for (const mapping of map.mappings) {
29-
if (filter(mapping.data)) {
30-
const mappingStart = mapping.sourceOffsets[0];
31-
const mappingEnd = mapping.sourceOffsets[mapping.sourceOffsets.length - 1] + mapping.lengths[mapping.lengths.length - 1];
32-
const overlap = getOverlapRange(start, end, mappingStart, mappingEnd);
33-
if (overlap) {
34-
const curMappedStart = (overlap.start - mappingStart) + mapping.generatedOffsets[0];
35-
const lastGeneratedLength = (mapping.generatedLengths ?? mapping.lengths)[mapping.generatedOffsets.length - 1];
36-
const curMappedEndOffset = Math.min(overlap.end - mapping.sourceOffsets[mapping.sourceOffsets.length - 1], lastGeneratedLength);
37-
const curMappedEnd = mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + curMappedEndOffset;
38-
39-
mappedStart = mappedStart === undefined ? curMappedStart : Math.min(mappedStart, curMappedStart);
40-
mappedEnd = mappedEnd === undefined ? curMappedEnd : Math.max(mappedEnd, curMappedEnd);
41-
}
42-
}
43-
}
44-
}
45-
46-
if (mappedStart !== undefined && mappedEnd !== undefined) {
47-
return {
48-
start: mappedStart,
49-
end: mappedEnd,
50-
};
51-
}
52-
}
53-
54-
function getOverlapRange(
55-
range1Start: number,
56-
range1End: number,
57-
range2Start: number,
58-
range2End: number
59-
): { start: number, end: number; } | undefined {
60-
61-
const start = Math.max(range1Start, range2Start);
62-
const end = Math.min(range1End, range2End);
63-
64-
if (start > end) {
65-
return undefined;
66-
}
67-
68-
return {
69-
start,
70-
end,
71-
};
72-
}
73-
744
export function isInsideRange(parent: vscode.Range, child: vscode.Range) {
755
if (child.start.line < parent.start.line) {
766
return false;

packages/typescript/lib/node/proxyLanguageService.ts

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {
22
CodeInformation,
3-
Language,
3+
findOverlapCodeRange,
44
isCallHierarchyEnabled,
55
isCodeActionsEnabled,
66
isCompletionEnabled,
@@ -16,6 +16,7 @@ import {
1616
isSemanticTokensEnabled,
1717
isSignatureHelpEnabled,
1818
isTypeDefinitionEnabled,
19+
Language,
1920
} from '@volar/language-core';
2021
import type * as ts from 'typescript';
2122
import { dedupeDocumentSpans } from './dedupe';
@@ -613,23 +614,17 @@ function getEncodedSemanticClassifications(language: Language<string>, getEncode
613614
};
614615
}
615616
if (serviceScript) {
616-
let start: number | undefined;
617-
let end: number | undefined;
618617
const map = language.maps.get(serviceScript.code, targetScript);
619-
for (const mapping of map.mappings) {
620-
// TODO reuse the logic from language service
621-
if (isSemanticTokensEnabled(mapping.data) && mapping.sourceOffsets[0] >= span.start && mapping.sourceOffsets[0] <= span.start + span.length) {
622-
start ??= mapping.generatedOffsets[0];
623-
end ??= mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + (mapping.generatedLengths ?? mapping.lengths)[mapping.lengths.length - 1];
624-
start = Math.min(start, mapping.generatedOffsets[0]);
625-
end = Math.max(end, mapping.generatedOffsets[mapping.generatedOffsets.length - 1] + (mapping.generatedLengths ?? mapping.lengths)[mapping.lengths.length - 1]);
626-
}
618+
const mapped = findOverlapCodeRange(span.start, span.start + span.length, map, isSemanticTokensEnabled);
619+
if (!mapped) {
620+
return {
621+
spans: [],
622+
endOfLineState: 0
623+
};
627624
}
628-
start ??= 0;
629-
end ??= targetScript.snapshot.getLength();
630625
const mappingOffset = getMappingOffset(language, serviceScript);
631-
start += mappingOffset;
632-
end += mappingOffset;
626+
const start = mapped.start + mappingOffset;
627+
const end = mapped.end + mappingOffset;
633628
const result = getEncodedSemanticClassifications(targetScript.id, { start, length: end - start }, format);
634629
const spans: number[] = [];
635630
for (let i = 0; i < result.spans.length; i += 3) {
@@ -1085,3 +1080,4 @@ function displayPartsToString(displayParts: ts.SymbolDisplayPart[] | undefined)
10851080
}
10861081
return '';
10871082
}
1083+

0 commit comments

Comments
 (0)