mirror of
https://github.com/microsoft/TypeScript.git
synced 2025-11-18 17:21:48 +00:00
Add more tests, special handling for mapped types
This commit is contained in:
+49
-10
@@ -1744,6 +1744,14 @@ namespace ts.server {
|
||||
};
|
||||
}
|
||||
|
||||
private locationsAreEqual(a: protocol.Location, b: protocol.Location): boolean {
|
||||
return a.line === b.line && a.offset === b.offset;
|
||||
}
|
||||
|
||||
private locationTextSpansAreEqual(a: protocol.TextSpan, b: protocol.TextSpan): boolean {
|
||||
return this.locationsAreEqual(a.start, b.start) && this.locationsAreEqual(a.end, b.end);
|
||||
}
|
||||
|
||||
private getNavigationTree(args: protocol.FileRequestArgs, simplifiedResult: boolean): protocol.NavigationTree | NavigationTree | undefined {
|
||||
const { file, languageService } = this.getFileAndLanguageServiceForSyntacticOperation(args);
|
||||
const tree = languageService.getNavigationTree(file);
|
||||
@@ -2066,12 +2074,22 @@ namespace ts.server {
|
||||
const sourceFile = languageService.getNonBoundSourceFile(file);
|
||||
const scriptInfo = Debug.assertDefined(this.projectService.getScriptInfo(file));
|
||||
const fullTextSpan = this.toLocationTextSpan(
|
||||
createTextSpan(sourceFile.getFullStart(), sourceFile.getEnd() - sourceFile.getFullStart()),
|
||||
createTextSpanFromBounds(sourceFile.getFullStart(), sourceFile.getEnd()),
|
||||
scriptInfo);
|
||||
|
||||
return map(locations, location => {
|
||||
const pos = this.getPosition(location, scriptInfo);
|
||||
let selectionRange: protocol.SelectionRange = { textSpan: fullTextSpan };
|
||||
const pushSelectionRange = (textSpan: protocol.TextSpan, syntaxKind?: SyntaxKind): void => {
|
||||
// Skip ranges that are identical to the parent
|
||||
if (!this.locationTextSpansAreEqual(textSpan, selectionRange.textSpan)) {
|
||||
selectionRange = { textSpan, parent: selectionRange };
|
||||
if (syntaxKind) {
|
||||
Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(syntaxKind) });
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Skip top-level SyntaxList
|
||||
let current: Node | undefined = sourceFile.getChildAt(0);
|
||||
while (true) {
|
||||
@@ -2097,16 +2115,37 @@ namespace ts.server {
|
||||
&& nextNode && nextNode.kind === SyntaxKind.CloseBraceToken;
|
||||
const start = isBetweenBraces ? prevNode.getEnd() : node.getStart();
|
||||
const end = isBetweenBraces ? nextNode.getStart() : node.getEnd();
|
||||
const textSpan = this.toLocationTextSpan(createTextSpan(start, end - start), scriptInfo);
|
||||
current = node;
|
||||
// Skip ranges that are identical to the parent
|
||||
if (selectionRange.textSpan.start !== textSpan.start || selectionRange.textSpan.end !== textSpan.end) {
|
||||
selectionRange = {
|
||||
textSpan,
|
||||
parent: selectionRange,
|
||||
};
|
||||
Object.defineProperty(selectionRange, "__debugKind", { value: formatSyntaxKind(node.kind) });
|
||||
const textSpan = this.toLocationTextSpan(createTextSpanFromBounds(start, end), scriptInfo);
|
||||
pushSelectionRange(textSpan, node.kind);
|
||||
|
||||
// Mapped types _look_ like ObjectTypes with a single member,
|
||||
// but in fact don’t contain a SyntaxList or a node containing
|
||||
// the “key/value” pair like ObjectTypes do, but it seems intuitive
|
||||
// that the selection would snap to those points. The philosophy
|
||||
// of choosing a selection range is not so much about what the
|
||||
// syntax currently _is_ as what the syntax might easily become
|
||||
// if the user is making a selection; e.g., we synthesize a selection
|
||||
// around the “key/value” pair not because there’s a node there, but
|
||||
// because it allows the mapped type to become an object type with a
|
||||
// few keystrokes.
|
||||
if (isMappedTypeNode(node)) {
|
||||
const openBraceToken = Debug.assertDefined(node.getFirstToken());
|
||||
const firstNonBraceToken = Debug.assertDefined(node.getChildAt(1));
|
||||
const closeBraceToken = Debug.assertDefined(node.getLastToken());
|
||||
Debug.assertEqual(openBraceToken.kind, SyntaxKind.OpenBraceToken);
|
||||
Debug.assertEqual(closeBraceToken.kind, SyntaxKind.CloseBraceToken);
|
||||
const spanWithoutBraces = this.toLocationTextSpan(createTextSpanFromBounds(
|
||||
openBraceToken.getEnd(),
|
||||
closeBraceToken.getStart(),
|
||||
), scriptInfo);
|
||||
const spanWithoutBracesOrTrivia = this.toLocationTextSpan(createTextSpanFromBounds(
|
||||
firstNonBraceToken.getStart(),
|
||||
closeBraceToken.getFullStart(),
|
||||
), scriptInfo);
|
||||
pushSelectionRange(spanWithoutBraces);
|
||||
pushSelectionRange(spanWithoutBracesOrTrivia);
|
||||
}
|
||||
current = node;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,61 +24,175 @@ class Foo {
|
||||
}
|
||||
}`);
|
||||
|
||||
const locations = getSelectionRange([{
|
||||
line: 4,
|
||||
offset: 13
|
||||
}]);
|
||||
const locations = getSelectionRange([
|
||||
{
|
||||
line: 4,
|
||||
offset: 13,
|
||||
}, {
|
||||
line: 5,
|
||||
offset: 22,
|
||||
},
|
||||
]);
|
||||
|
||||
// Common to results for both locations
|
||||
const ifStatementUp: protocol.SelectionRange = {
|
||||
textSpan: { // IfStatement
|
||||
start: { line: 4, offset: 9 },
|
||||
end: { line: 6, offset: 10 } },
|
||||
parent: {
|
||||
textSpan: { // SyntaxList + whitespace (body of method)
|
||||
start: { line: 3, offset: 16 },
|
||||
end: { line: 8, offset: 5 } },
|
||||
parent: {
|
||||
textSpan: { // MethodDeclaration
|
||||
start: { line: 3, offset: 5 },
|
||||
end: { line: 8, offset: 6 } },
|
||||
parent: {
|
||||
textSpan: { // SyntaxList + whitespace (body of class)
|
||||
start: { line: 2, offset: 12 },
|
||||
end: { line: 9, offset: 1 } },
|
||||
parent: {
|
||||
textSpan: { // ClassDeclaration
|
||||
start: { line: 2, offset: 1 },
|
||||
end: { line: 9, offset: 2 } },
|
||||
parent: {
|
||||
textSpan: { // SourceFile (all text)
|
||||
start: { line: 1, offset: 1 },
|
||||
end: { line: 9, offset: 2 }, } } } } } } };
|
||||
|
||||
assert.deepEqual(locations, [
|
||||
{
|
||||
textSpan: { // a
|
||||
start: { line: 4, offset: 13 },
|
||||
end: { line: 4, offset: 14 },
|
||||
},
|
||||
end: { line: 4, offset: 14 } },
|
||||
parent: {
|
||||
textSpan: { // a === b
|
||||
start: { line: 4, offset: 13 },
|
||||
end: { line: 4, offset: 20 },
|
||||
},
|
||||
end: { line: 4, offset: 20 } },
|
||||
parent: ifStatementUp } },
|
||||
{
|
||||
textSpan: { // true
|
||||
start: { line: 5, offset: 20 },
|
||||
end: { line: 5, offset: 24 } },
|
||||
parent: {
|
||||
textSpan: { // return true;
|
||||
start: { line: 5, offset: 13 },
|
||||
end: { line: 5, offset: 25 } },
|
||||
parent: {
|
||||
textSpan: { // IfStatement
|
||||
start: { line: 4, offset: 9 },
|
||||
end: { line: 6, offset: 10 },
|
||||
},
|
||||
parent: {
|
||||
textSpan: { // SyntaxList + whitespace (body of method)
|
||||
start: { line: 3, offset: 16 },
|
||||
end: { line: 8, offset: 5 },
|
||||
},
|
||||
parent: {
|
||||
textSpan: { // MethodDeclaration
|
||||
start: { line: 3, offset: 5 },
|
||||
end: { line: 8, offset: 6 },
|
||||
},
|
||||
parent: {
|
||||
textSpan: { // SyntaxList + whitespace (body of class)
|
||||
start: { line: 2, offset: 12 },
|
||||
end: { line: 9, offset: 1 },
|
||||
},
|
||||
parent: {
|
||||
textSpan: { // ClassDeclaration
|
||||
start: { line: 2, offset: 1 },
|
||||
end: { line: 9, offset: 2 },
|
||||
},
|
||||
parent: {
|
||||
textSpan: { // SourceFile (all text)
|
||||
start: { line: 1, offset: 1 },
|
||||
end: { line: 9, offset: 2 },
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
textSpan: { // SyntaxList + whitespace (body of IfStatement)
|
||||
start: { line: 4, offset: 23 },
|
||||
end: { line: 6, offset: 9 } },
|
||||
parent: ifStatementUp } } }
|
||||
]);
|
||||
});
|
||||
|
||||
it("works for simple TypeScript", () => {
|
||||
const getSelectionRange = setup("/file.ts", `
|
||||
export interface IService {
|
||||
_serviceBrand: any;
|
||||
|
||||
open(host: number, data: any): Promise<any>;
|
||||
}`);
|
||||
const locations = getSelectionRange([
|
||||
{
|
||||
line: 5,
|
||||
offset: 12,
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepEqual(locations, [
|
||||
{
|
||||
textSpan: { // host
|
||||
start: { line: 5, offset: 10 },
|
||||
end: { line: 5, offset: 14 } },
|
||||
parent: {
|
||||
textSpan: { // host: number
|
||||
start: { line: 5, offset: 10 },
|
||||
end: { line: 5, offset: 22 } },
|
||||
parent: {
|
||||
textSpan: { // host: number, data: any
|
||||
start: { line: 5, offset: 10 },
|
||||
end: { line: 5, offset: 33 } },
|
||||
parent: {
|
||||
textSpan: { // open(host: number, data: any): Promise<any>;
|
||||
start: { line: 5, offset: 5 },
|
||||
end: { line: 5, offset: 49 } },
|
||||
parent: {
|
||||
textSpan: { // SyntaxList + whitespace (body of interface)
|
||||
start: { line: 2, offset: 28 },
|
||||
end: { line: 6, offset: 1 } },
|
||||
parent: {
|
||||
textSpan: { // InterfaceDeclaration
|
||||
start: { line: 2, offset: 1 },
|
||||
end: { line: 6, offset: 2 } },
|
||||
parent: {
|
||||
textSpan: { // SourceFile
|
||||
start: { line: 1, offset: 1 },
|
||||
end: { line: 6, offset: 2 } } } } } } } } },
|
||||
]);
|
||||
});
|
||||
|
||||
it("works for complex TypeScript", () => {
|
||||
const getSelectionRange = setup("/file.ts", `
|
||||
type X<T, P> = IsExactlyAny<P> extends true ? T : ({ [K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K]; } & Pick<T, Exclude<keyof T, keyof P>>)
|
||||
`);
|
||||
const locations = getSelectionRange([
|
||||
{
|
||||
line: 2,
|
||||
offset: 133,
|
||||
},
|
||||
]);
|
||||
|
||||
assert.deepEqual(locations, [
|
||||
{
|
||||
textSpan: { // K
|
||||
start: { line: 2, offset: 133 },
|
||||
end: { line: 2, offset: 134 } },
|
||||
parent: {
|
||||
textSpan: { // P[K]
|
||||
start: { line: 2, offset: 131 },
|
||||
end: { line: 2, offset: 135 } },
|
||||
parent: {
|
||||
textSpan: { // K extends keyof T ? T[K] : P[K]
|
||||
start: { line: 2, offset: 104 },
|
||||
end: { line: 2, offset: 135 } },
|
||||
parent: {
|
||||
textSpan: { // IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K]
|
||||
start: { line: 2, offset: 70 },
|
||||
end: { line: 2, offset: 142 } },
|
||||
parent: {
|
||||
textSpan: { // [K in keyof P]: IsExactlyAny<P[K]> extends true ? K extends keyof T ? T[K] : P[K] : P[K];
|
||||
start: { line: 2, offset: 54 },
|
||||
end: { line: 2, offset: 143 } },
|
||||
parent: { // same as above + whitespace
|
||||
textSpan: {
|
||||
start: { line: 2, offset: 53 },
|
||||
end: { line: 2, offset: 144 } },
|
||||
parent: {
|
||||
textSpan: { // MappedType: same as above + braces
|
||||
start: { line: 2, offset: 52 },
|
||||
end: { line: 2, offset: 145 } },
|
||||
parent: {
|
||||
textSpan: { // IntersectionType: { [K in keyof P]: ... } & Pick<T, Exclude<keyof T, keyof P>>
|
||||
start: { line: 2, offset: 52 },
|
||||
end: { line: 2, offset: 182 } },
|
||||
parent: {
|
||||
textSpan: { // same as above + parens
|
||||
start: { line: 2, offset: 51 },
|
||||
end: { line: 2, offset: 183 } },
|
||||
parent: {
|
||||
textSpan: { // Whole TypeNode of TypeAliasDeclaration
|
||||
start: { line: 2, offset: 16 },
|
||||
end: { line: 2, offset: 183 } },
|
||||
parent: {
|
||||
textSpan: { // Whole TypeAliasDeclaration
|
||||
start: { line: 2, offset: 1 },
|
||||
end: { line: 2, offset: 183 } },
|
||||
parent: {
|
||||
textSpan: { // SourceFile
|
||||
start: { line: 1, offset: 1 },
|
||||
end: { line: 2, offset: 184 } } } } } } } } } } } } } },
|
||||
]);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user